diff --git a/hst-frontend-new/.gitignore b/hst-frontend-new/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/hst-frontend-new/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/hst-frontend-new/README.md b/hst-frontend-new/README.md new file mode 100644 index 0000000..9f156e0 --- /dev/null +++ b/hst-frontend-new/README.md @@ -0,0 +1,184 @@ +# DECK Frontend - Vite + TypeScript + +Frontend modular para visualización de tags semánticos TZZR (HST/FLG/ITM/LOC/PLY). + +## Stack + +- **Vite** - Bundler con HMR +- **TypeScript** - Tipado estático +- **ES6 Modules** - import/export +- **D3.js** - Lazy loading solo en GraphView + +## Estructura + +``` +src/ +├── main.ts # Bootstrap y App principal +├── types/ # Interfaces TypeScript +│ ├── tag.ts # Tag, Group, Library, ChildTag, RelatedTag +│ ├── state.ts # AppState, ViewType, BaseType +│ └── graph.ts # GraphNode, GraphEdge, EdgeType +├── config/ # Constantes +│ ├── api.ts # API_BASE +│ ├── categories.ts # CATS (colores por categoría) +│ └── edges.ts # EDGE_COLORS +├── state/ # Store reactivo +│ ├── store.ts # createStore() +│ └── index.ts # Estado inicial +├── api/ # Fetch tipado +│ ├── client.ts # apiClient(), apiClientSafe() +│ ├── tags.ts # fetchTags, fetchHstTags, fetchChildren, fetchRelated +│ ├── groups.ts # fetchGroups +│ ├── libraries.ts # fetchLibraries, fetchLibraryMembers +│ └── graph.ts # fetchGraphEdges, fetchTreeEdges +├── utils/ # Helpers puros +│ ├── dom.ts # $, $$, createElement, delegateEvent +│ ├── i18n.ts # getName, getImg, createNameMap, resolveGroupName +│ ├── filters.ts # filterTags +│ ├── clipboard.ts # copyToClipboard, copyMrf +│ └── toast.ts # toast() +├── components/ # Componentes reutilizables +│ ├── Component.ts # Clase base +│ ├── Card/ +│ ├── TagChip/ +│ └── Toast/ +├── views/ # Vistas principales +│ ├── View.ts # Clase base +│ ├── GridView/ # Vista de tarjetas +│ ├── TreeView/ # Vista de árbol agrupado +│ ├── GraphView/ # Grafo D3 (lazy load) +│ └── DetailPanel/ # Panel lateral de detalle +├── router/ # Hash routing +│ └── index.ts # Router con parseHash/updateHash +└── styles/ + └── main.css # Estilos globales +``` + +## Resolución de Nombres + +### Prioridad de campos para mostrar nombres + +1. `name_es` / `name_en` / `name_ch` (según idioma) +2. `alias` +3. `ref` +4. `mrf.slice(0, 8)` (fallback a hash truncado) + +### Resolución de grupos (set_hst) + +El campo `set_hst` contiene el MRF de un tag HST que representa la categoría. +Para resolver el nombre del grupo: + +1. Se cargan siempre los tags de HST (`fetchHstTags`) +2. Se crea un mapa `mrf → nombre` con `createNameMap(hstTags, lang)` +3. Se resuelve el nombre con `resolveGroupName(set_hst, nameMap)` + +Esto permite que al ver cualquier base (flg, itm, loc, ply), los grupos +muestren nombres legibles en lugar de hashes. + +## API Endpoints + +| Endpoint | Descripción | +|----------|-------------| +| `GET /api/{base}` | Lista tags (hst, flg, itm, loc, ply) | +| `GET /api/hst?select=...` | Tags HST para resolver nombres | +| `POST /api/rpc/api_children` | Hijos de un tag | +| `POST /api/rpc/api_related` | Tags relacionados | +| `GET /api/graph_hst` | Relaciones del grafo | +| `GET /api/tree_hst` | Jerarquías | +| `GET /api/groups` | Grupos disponibles | +| `GET /api/library_hst` | Bibliotecas | +| `POST /api/rpc/library_members` | Miembros de biblioteca | + +## Comandos + +```bash +# Desarrollo +npm run dev + +# Build producción +npm run build + +# Preview build +npm run preview + +# Lint +npm run lint +``` + +## Despliegue + +```bash +# Build +npm run build + +# Deploy a DECK +scp -r dist/* root@72.62.1.113:/opt/hst/web/ +``` + +## URLs + +- **DECK**: https://tzzrdeck.me +- **HST**: https://hst.tzzrdeck.me +- **API**: https://tzzrdeck.me/api/ + +## Layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TOPBAR: Logo | Lang | API | SEL | GET | Bases | Search | View │ +├─────────────────────────────────────────────────────────────┤ +│ GROUPS BAR: Todos | Grupo1 | Grupo2 | ... │ +├────────┬────────────────────────────────────┬───────────────┤ +│ │ │ │ +│ LEFT │ CONTENT AREA │ DETAIL │ +│ PANEL │ (Grid / Tree / Graph) │ PANEL │ +│ │ │ │ +│ Libs │ │ (slide-in) │ +│ │ │ │ +└────────┴────────────────────────────────────┴───────────────┘ +``` + +## State + +```typescript +interface AppState { + // Navegación + base: 'hst' | 'flg' | 'itm' | 'loc' | 'ply'; + lang: 'es' | 'en' | 'ch'; + view: 'grid' | 'tree' | 'graph'; + + // Filtros + search: string; + group: string; // MRF del grupo o 'all' + library: string; // MRF de biblioteca o 'all' + libraryMembers: Set; + + // Selección + selectionMode: boolean; + selected: Set; + selectedTag: Tag | null; + + // Datos + tags: Tag[]; // Tags de la base actual + hstTags: Tag[]; // Tags HST para resolver nombres de grupos + groups: Group[]; + libraries: Library[]; + graphEdges: GraphEdge[]; + treeEdges: TreeEdge[]; + + // Grafo + graphFilters: { cats: Set, edges: Set }; + graphSettings: { nodeSize, linkDist, showImg, showLbl }; +} +``` + +## Changelog + +### 2026-01-13 + +- Migrado de vanilla JS a Vite + TypeScript +- Añadido panel izquierdo de bibliotecas +- Implementada resolución de nombres para grupos (set_hst → nombre) +- Prioridad de nombres: name_es → alias → ref → hash +- D3 lazy loading en GraphView +- Layout de 3 paneles (left, center, detail) diff --git a/hst-frontend-new/index.html b/hst-frontend-new/index.html new file mode 100644 index 0000000..108aa04 --- /dev/null +++ b/hst-frontend-new/index.html @@ -0,0 +1,101 @@ + + + + DECK + + + + +
+ +
+
+ + + + + + +
+
+
+ + + + + +
+
+
+ +
+ + + +
+
+
+ + +
+ + +
+ +
+
ALL
+
+ + +
+
+
Cargando...
+
+
+ + +
+
+ + + +
+ + + + diff --git a/hst-frontend-new/package-lock.json b/hst-frontend-new/package-lock.json new file mode 100644 index 0000000..d5f0606 --- /dev/null +++ b/hst-frontend-new/package-lock.json @@ -0,0 +1,1872 @@ +{ + "name": "hst-frontend-new", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hst-frontend-new", + "version": "0.0.0", + "dependencies": { + "d3": "^7.9.0" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "typescript": "~5.9.3", + "vite": "^7.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/hst-frontend-new/package.json b/hst-frontend-new/package.json new file mode 100644 index 0000000..7459325 --- /dev/null +++ b/hst-frontend-new/package.json @@ -0,0 +1,19 @@ +{ + "name": "deck-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "typescript": "~5.9.3", + "vite": "^7.2.4" + }, + "dependencies": { + "d3": "^7.9.0" + } +} diff --git a/hst-frontend-new/src/api/client.ts b/hst-frontend-new/src/api/client.ts new file mode 100644 index 0000000..3245d67 --- /dev/null +++ b/hst-frontend-new/src/api/client.ts @@ -0,0 +1,38 @@ +import { API_BASE } from '@/config/index.ts'; + +interface FetchOptions { + method?: 'GET' | 'POST'; + body?: Record; +} + +export async function apiClient( + endpoint: string, + options: FetchOptions = {} +): Promise { + const { method = 'GET', body } = options; + + const config: RequestInit = { + method, + headers: body ? { 'Content-Type': 'application/json' } : undefined, + body: body ? JSON.stringify(body) : undefined, + }; + + const response = await fetch(`${API_BASE}${endpoint}`, config); + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + return response.json(); +} + +export async function apiClientSafe( + endpoint: string, + options: FetchOptions = {}, + fallback: T +): Promise { + try { + return await apiClient(endpoint, options); + } catch { + console.error(`API call failed: ${endpoint}`); + return fallback; + } +} diff --git a/hst-frontend-new/src/api/graph.ts b/hst-frontend-new/src/api/graph.ts new file mode 100644 index 0000000..22b0b66 --- /dev/null +++ b/hst-frontend-new/src/api/graph.ts @@ -0,0 +1,8 @@ +import { apiClientSafe } from './client.ts'; +import type { GraphEdge, TreeEdge } from '@/types/index.ts'; + +export const fetchGraphEdges = (): Promise => + apiClientSafe('/graph_hst', {}, []); + +export const fetchTreeEdges = (): Promise => + apiClientSafe('/tree_hst', {}, []); diff --git a/hst-frontend-new/src/api/groups.ts b/hst-frontend-new/src/api/groups.ts new file mode 100644 index 0000000..af08297 --- /dev/null +++ b/hst-frontend-new/src/api/groups.ts @@ -0,0 +1,5 @@ +import { apiClientSafe } from './client.ts'; +import type { Group } from '@/types/index.ts'; + +export const fetchGroups = (): Promise => + apiClientSafe('/api_groups', {}, []); diff --git a/hst-frontend-new/src/api/index.ts b/hst-frontend-new/src/api/index.ts new file mode 100644 index 0000000..8d6cd78 --- /dev/null +++ b/hst-frontend-new/src/api/index.ts @@ -0,0 +1,5 @@ +export { apiClient, apiClientSafe } from './client.ts'; +export { fetchTags, fetchHstTags, fetchChildren, fetchRelated } from './tags.ts'; +export { fetchGroups } from './groups.ts'; +export { fetchLibraries, fetchLibraryMembers } from './libraries.ts'; +export { fetchGraphEdges, fetchTreeEdges } from './graph.ts'; diff --git a/hst-frontend-new/src/api/libraries.ts b/hst-frontend-new/src/api/libraries.ts new file mode 100644 index 0000000..29d3c05 --- /dev/null +++ b/hst-frontend-new/src/api/libraries.ts @@ -0,0 +1,14 @@ +import { apiClientSafe } from './client.ts'; +import type { Library } from '@/types/index.ts'; + +export const fetchLibraries = (): Promise => + apiClientSafe('/api_library_list', {}, []); + +export const fetchLibraryMembers = async (mrf: string): Promise => { + const data = await apiClientSafe>( + `/library_hst?mrf_library=eq.${mrf}`, + {}, + [] + ); + return data.map(d => d.mrf_tag); +}; diff --git a/hst-frontend-new/src/api/tags.ts b/hst-frontend-new/src/api/tags.ts new file mode 100644 index 0000000..34c2d83 --- /dev/null +++ b/hst-frontend-new/src/api/tags.ts @@ -0,0 +1,21 @@ +import { apiClientSafe } from './client.ts'; +import type { Tag, ChildTag, RelatedTag, BaseType } from '@/types/index.ts'; + +export const fetchTags = (base: BaseType): Promise => + apiClientSafe(`/${base}?order=ref.asc`, {}, []); + +// Fetch HST tags for group name resolution (set_hst points to hst tags) +export const fetchHstTags = (): Promise => + apiClientSafe('/hst?select=mrf,ref,alias,name_es,name_en,name_ch', {}, []); + +export const fetchChildren = (mrf: string): Promise => + apiClientSafe('/rpc/api_children', { + method: 'POST', + body: { parent_mrf: mrf } + }, []); + +export const fetchRelated = (mrf: string): Promise => + apiClientSafe('/rpc/api_related', { + method: 'POST', + body: { tag_mrf: mrf } + }, []); diff --git a/hst-frontend-new/src/components/Card/Card.ts b/hst-frontend-new/src/components/Card/Card.ts new file mode 100644 index 0000000..318db1d --- /dev/null +++ b/hst-frontend-new/src/components/Card/Card.ts @@ -0,0 +1,47 @@ +import { Component } from '../Component.ts'; +import type { Tag, LangType } from '@/types/index.ts'; +import { getName, getImg } from '@/utils/index.ts'; + +export interface CardProps { + tag: Tag; + lang: LangType; + selected: boolean; + selectionMode: boolean; + onClick: (mrf: string) => void; + onSelect: (mrf: string) => void; +} + +export class Card extends Component { + protected template(): string { + const { tag, lang, selected, selectionMode } = this.props; + const img = getImg(tag); + const name = getName(tag, lang); + + return ` +
+ ${selectionMode ? ` + + ` : ''} + ${img + ? `${tag.ref}` + : `
${tag.ref?.slice(0, 2) || 'T'}
` + } +
${name}
+
+ `; + } + + protected bindEvents(): void { + const { onClick, onSelect, selectionMode } = this.props; + const mrf = this.props.tag.mrf; + + this.element.addEventListener('click', (e) => { + if (selectionMode) { + e.preventDefault(); + onSelect(mrf); + } else { + onClick(mrf); + } + }); + } +} diff --git a/hst-frontend-new/src/components/Component.ts b/hst-frontend-new/src/components/Component.ts new file mode 100644 index 0000000..f68cf52 --- /dev/null +++ b/hst-frontend-new/src/components/Component.ts @@ -0,0 +1,42 @@ +export abstract class Component

{ + protected element: HTMLElement; + protected props: P; + + constructor(props: P) { + this.props = props; + this.element = this.createElement(); + this.bindEvents(); + } + + protected abstract template(): string; + + protected createElement(): HTMLElement { + const wrapper = document.createElement('div'); + wrapper.innerHTML = this.template().trim(); + return wrapper.firstElementChild as HTMLElement; + } + + protected bindEvents(): void { + // Override in subclasses + } + + public mount(container: HTMLElement): void { + container.appendChild(this.element); + } + + public unmount(): void { + this.element.remove(); + } + + public update(props: Partial

): void { + this.props = { ...this.props, ...props }; + const newElement = this.createElement(); + this.element.replaceWith(newElement); + this.element = newElement; + this.bindEvents(); + } + + public getElement(): HTMLElement { + return this.element; + } +} diff --git a/hst-frontend-new/src/components/Modal/Modal.ts b/hst-frontend-new/src/components/Modal/Modal.ts new file mode 100644 index 0000000..d3b0bb8 --- /dev/null +++ b/hst-frontend-new/src/components/Modal/Modal.ts @@ -0,0 +1,46 @@ +import { Component } from '../Component.ts'; + +export interface ModalProps { + title: string; + content: string; + isOpen: boolean; + onClose: () => void; +} + +export class Modal extends Component { + protected template(): string { + const { title, content, isOpen } = this.props; + return ` +

+ `; + } + + protected bindEvents(): void { + const closeBtn = this.element.querySelector('.modal-close'); + closeBtn?.addEventListener('click', this.props.onClose); + + this.element.addEventListener('click', (e) => { + if (e.target === this.element) { + this.props.onClose(); + } + }); + } + + public open(): void { + this.element.classList.add('open'); + } + + public close(): void { + this.element.classList.remove('open'); + } +} diff --git a/hst-frontend-new/src/components/TagChip/TagChip.ts b/hst-frontend-new/src/components/TagChip/TagChip.ts new file mode 100644 index 0000000..271bc5e --- /dev/null +++ b/hst-frontend-new/src/components/TagChip/TagChip.ts @@ -0,0 +1,25 @@ +import { Component } from '../Component.ts'; + +export interface TagChipProps { + mrf: string; + label: string; + title?: string; + onClick: (mrf: string) => void; +} + +export class TagChip extends Component { + protected template(): string { + const { mrf, label, title } = this.props; + return ` + + ${label} + + `; + } + + protected bindEvents(): void { + this.element.addEventListener('click', () => { + this.props.onClick(this.props.mrf); + }); + } +} diff --git a/hst-frontend-new/src/components/index.ts b/hst-frontend-new/src/components/index.ts new file mode 100644 index 0000000..df484ec --- /dev/null +++ b/hst-frontend-new/src/components/index.ts @@ -0,0 +1,4 @@ +export { Component } from './Component.ts'; +export { Card, type CardProps } from './Card/Card.ts'; +export { TagChip, type TagChipProps } from './TagChip/TagChip.ts'; +export { Modal, type ModalProps } from './Modal/Modal.ts'; diff --git a/hst-frontend-new/src/config/api.ts b/hst-frontend-new/src/config/api.ts new file mode 100644 index 0000000..7030377 --- /dev/null +++ b/hst-frontend-new/src/config/api.ts @@ -0,0 +1 @@ +export const API_BASE = '/api'; diff --git a/hst-frontend-new/src/config/categories.ts b/hst-frontend-new/src/config/categories.ts new file mode 100644 index 0000000..a19de89 --- /dev/null +++ b/hst-frontend-new/src/config/categories.ts @@ -0,0 +1,15 @@ +import type { CategoryKey } from '@/types/index.ts'; + +export interface CategoryConfig { + name: string; + color: string; +} + +export const CATS: Record = { + hst: { name: 'Hashtags', color: '#7c8aff' }, + spe: { name: 'Specs', color: '#FF9800' }, + vue: { name: 'Values', color: '#00BCD4' }, + vsn: { name: 'Visions', color: '#E91E63' }, + msn: { name: 'Missions', color: '#9C27B0' }, + flg: { name: 'Flags', color: '#4CAF50' } +}; diff --git a/hst-frontend-new/src/config/edges.ts b/hst-frontend-new/src/config/edges.ts new file mode 100644 index 0000000..ab64dd8 --- /dev/null +++ b/hst-frontend-new/src/config/edges.ts @@ -0,0 +1,14 @@ +import type { EdgeType } from '@/types/index.ts'; + +export const EDGE_COLORS: Record = { + relation: '#8BC34A', + specialization: '#9C27B0', + mirror: '#607D8B', + dependency: '#2196F3', + sequence: '#4CAF50', + composition: '#FF9800', + hierarchy: '#E91E63', + library: '#00BCD4', + contextual: '#FFC107', + association: '#795548' +}; diff --git a/hst-frontend-new/src/config/index.ts b/hst-frontend-new/src/config/index.ts new file mode 100644 index 0000000..23dfe12 --- /dev/null +++ b/hst-frontend-new/src/config/index.ts @@ -0,0 +1,3 @@ +export { CATS, type CategoryConfig } from './categories.ts'; +export { EDGE_COLORS } from './edges.ts'; +export { API_BASE } from './api.ts'; diff --git a/hst-frontend-new/src/main.ts b/hst-frontend-new/src/main.ts new file mode 100644 index 0000000..a9baba3 --- /dev/null +++ b/hst-frontend-new/src/main.ts @@ -0,0 +1,310 @@ +import { store } from '@/state/index.ts'; +import { Router } from '@/router/index.ts'; +import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts'; +import { GridView, TreeView, GraphView, DetailPanel } from '@/views/index.ts'; +import { $, $$, delegateEvent, toast, createNameMap, resolveGroupName } from '@/utils/index.ts'; +import type { BaseType, ViewType } from '@/types/index.ts'; +import './styles/main.css'; + +class App { + private router: Router; + private currentView: GridView | TreeView | GraphView | null = null; + private detailPanel: DetailPanel | null = null; + + constructor() { + this.router = new Router(store, () => this.init()); + } + + async start(): Promise { + this.router.parseHash(); + await this.init(); + this.bindEvents(); + } + + private async init(): Promise { + const contentArea = $('#content-area'); + const detailPanelEl = $('#detail-panel'); + if (!contentArea || !detailPanelEl) return; + + // Update UI + this.updateBaseButtons(); + this.updateViewTabs(); + + // Show loading + contentArea.innerHTML = '
Cargando...
'; + + // Fetch data + const state = store.getState(); + const [tags, hstTags, groups, libraries] = await Promise.all([ + fetchTags(state.base), + fetchHstTags(), // Always load HST for group name resolution + fetchGroups(), + fetchLibraries() + ]); + store.setState({ tags, hstTags, groups, libraries }); + + // Render groups + this.renderGroups(); + this.renderLibraries(); + + // Setup detail panel + if (!this.detailPanel) { + this.detailPanel = new DetailPanel(detailPanelEl, store); + } + + // Render view + this.renderView(); + } + + private renderView(): void { + const contentArea = $('#content-area'); + if (!contentArea) return; + + const state = store.getState(); + const showDetail = (mrf: string) => this.detailPanel?.showDetail(mrf); + + // Unmount current view + this.currentView?.unmount(); + + // Clear and set class + contentArea.innerHTML = ''; + contentArea.className = `content-area ${state.view}-view`; + + // Mount new view + switch (state.view) { + case 'grid': + this.currentView = new GridView(contentArea, store, showDetail); + this.currentView.mount(); + break; + case 'tree': + this.currentView = new TreeView(contentArea, store, showDetail); + this.currentView.mount(); + break; + case 'graph': + this.currentView = new GraphView(contentArea, store, showDetail); + (this.currentView as GraphView).mount(); + break; + } + } + + private renderGroups(): void { + const container = $('#groups-bar'); + if (!container) return; + + const state = store.getState(); + // Use hstTags for group name resolution (set_hst points to hst tags) + const nameMap = createNameMap(state.hstTags, state.lang); + + // Count tags per group + const counts = new Map(); + state.tags.forEach(tag => { + const group = tag.set_hst || 'sin-grupo'; + counts.set(group, (counts.get(group) || 0) + 1); + }); + + // Sort by count and take top 20 + const sorted = Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20); + + container.innerHTML = ` + + ${sorted.map(([groupMrf, count]) => { + const groupName = resolveGroupName(groupMrf === 'sin-grupo' ? undefined : groupMrf, nameMap); + return ` + + `; + }).join('')} + `; + + delegateEvent(container, '.group-btn', 'click', (_, target) => { + const group = target.dataset.group || 'all'; + store.setState({ group }); + this.renderGroups(); + this.renderView(); + }); + } + + private renderLibraries(): void { + const container = $('#left-panel'); + if (!container) return; + + const state = store.getState(); + + container.innerHTML = ` +
+ ALL +
+ ${state.libraries.map(lib => { + const icon = lib.img_thumb_url || lib.icon_url || ''; + const name = lib.name || lib.name_es || lib.alias || lib.ref || lib.mrf.slice(0, 6); + return ` +
+ ${icon ? `` : ''} + ${name.slice(0, 8)} +
+ `; + }).join('')} + `; + + delegateEvent(container, '.lib-icon', 'click', async (_, target) => { + const library = target.dataset.lib || 'all'; + + if (library === 'all') { + store.setState({ library: 'all', libraryMembers: new Set() }); + } else { + const members = await fetchLibraryMembers(library); + store.setState({ library, libraryMembers: new Set(members) }); + } + + this.renderLibraries(); + this.renderView(); + }); + } + + private updateBaseButtons(): void { + const state = store.getState(); + $$('.base-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.base === state.base); + }); + } + + private updateViewTabs(): void { + const state = store.getState(); + $$('.view-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.view === state.view); + }); + } + + private bindEvents(): void { + // Base buttons + delegateEvent(document.body, '.base-btn', 'click', async (_, target) => { + const base = target.dataset.base as BaseType; + if (!base) return; + + store.setState({ + base, + group: 'all', + library: 'all', + libraryMembers: new Set(), + search: '', + graphEdges: [], + treeEdges: [], + selected: new Set(), + selectionMode: false + }); + + this.router.updateHash(); + await this.init(); + }); + + // View tabs + delegateEvent(document.body, '.view-tab', 'click', (_, target) => { + const view = target.dataset.view as ViewType; + if (!view) return; + + store.setState({ view }); + this.router.updateHash(); + this.detailPanel?.close(); + this.updateViewTabs(); + this.renderView(); + }); + + // Search + const searchInput = $('#search') as HTMLInputElement; + if (searchInput) { + let timeout: number; + searchInput.addEventListener('input', () => { + clearTimeout(timeout); + timeout = window.setTimeout(() => { + store.setState({ search: searchInput.value }); + this.renderView(); + }, 200); + }); + } + + // Language select + const langSelect = $('#lang-select') as HTMLSelectElement; + if (langSelect) { + langSelect.addEventListener('change', () => { + store.setState({ lang: langSelect.value as 'es' | 'en' | 'ch' }); + this.renderView(); + }); + } + + // Selection mode + const selBtn = $('#btn-sel'); + if (selBtn) { + selBtn.addEventListener('click', () => { + const state = store.getState(); + store.setState({ + selectionMode: !state.selectionMode, + selected: state.selectionMode ? new Set() : state.selected + }); + selBtn.classList.toggle('active', !state.selectionMode); + this.updateSelectionCount(); + this.renderView(); + }); + } + + // Get selected + const getBtn = $('#btn-get'); + if (getBtn) { + getBtn.addEventListener('click', () => { + const state = store.getState(); + if (state.selected.size === 0) { + toast('No hay seleccionados'); + return; + } + navigator.clipboard.writeText([...state.selected].join('\n')) + .then(() => toast(`Copiados ${state.selected.size} mrfs`)); + }); + } + + // API modal + const apiBtn = $('#btn-api'); + const apiModal = $('#api-modal'); + if (apiBtn && apiModal) { + apiBtn.addEventListener('click', () => apiModal.classList.add('open')); + apiModal.addEventListener('click', (e) => { + if (e.target === apiModal) apiModal.classList.remove('open'); + }); + const closeBtn = apiModal.querySelector('.modal-close'); + closeBtn?.addEventListener('click', () => apiModal.classList.remove('open')); + } + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + this.detailPanel?.close(); + $('#api-modal')?.classList.remove('open'); + if (store.getState().selectionMode) { + store.setState({ selectionMode: false, selected: new Set() }); + $('#btn-sel')?.classList.remove('active'); + this.renderView(); + } + } + if (e.key === '/' && (e.target as HTMLElement).tagName !== 'INPUT') { + e.preventDefault(); + ($('#search') as HTMLInputElement)?.focus(); + } + }); + } + + private updateSelectionCount(): void { + const counter = $('#sel-count'); + if (counter) { + const count = store.getState().selected.size; + counter.textContent = count > 0 ? `(${count})` : ''; + } + } +} + +// Bootstrap +document.addEventListener('DOMContentLoaded', () => { + new App().start(); +}); diff --git a/hst-frontend-new/src/router/index.ts b/hst-frontend-new/src/router/index.ts new file mode 100644 index 0000000..e6dd489 --- /dev/null +++ b/hst-frontend-new/src/router/index.ts @@ -0,0 +1 @@ +export { Router } from './router.ts'; diff --git a/hst-frontend-new/src/router/router.ts b/hst-frontend-new/src/router/router.ts new file mode 100644 index 0000000..27a33a9 --- /dev/null +++ b/hst-frontend-new/src/router/router.ts @@ -0,0 +1,62 @@ +import type { Store } from '@/state/store.ts'; +import type { AppState, BaseType, ViewType } from '@/types/index.ts'; + +const VALID_BASES: BaseType[] = ['hst', 'flg', 'itm', 'loc', 'ply']; +const VALID_VIEWS: ViewType[] = ['grid', 'tree', 'graph']; + +export class Router { + private store: Store; + private onNavigate: () => void; + + constructor(store: Store, onNavigate: () => void) { + this.store = store; + this.onNavigate = onNavigate; + + window.addEventListener('hashchange', () => this.handleHashChange()); + } + + parseHash(): void { + const hash = window.location.hash + .replace(/^#\/?/, '') + .replace(/\/?$/, '') + .split('/') + .filter(Boolean); + + const state = this.store.getState(); + let base = state.base; + let view = state.view; + + if (hash[0] && VALID_BASES.includes(hash[0].toLowerCase() as BaseType)) { + base = hash[0].toLowerCase() as BaseType; + } + + if (hash[1] && VALID_VIEWS.includes(hash[1].toLowerCase() as ViewType)) { + view = hash[1].toLowerCase() as ViewType; + } + + this.store.setState({ base, view }); + } + + updateHash(): void { + const state = this.store.getState(); + const parts: string[] = [state.base]; + if (state.view !== 'grid') { + parts.push(state.view); + } + window.location.hash = '/' + parts.join('/') + '/'; + } + + private handleHashChange(): void { + this.parseHash(); + this.onNavigate(); + } + + navigate(base?: BaseType, view?: ViewType): void { + const state = this.store.getState(); + this.store.setState({ + base: base ?? state.base, + view: view ?? state.view + }); + this.updateHash(); + } +} diff --git a/hst-frontend-new/src/state/index.ts b/hst-frontend-new/src/state/index.ts new file mode 100644 index 0000000..5d3a5c7 --- /dev/null +++ b/hst-frontend-new/src/state/index.ts @@ -0,0 +1,35 @@ +import { createStore } from './store.ts'; +import type { AppState, EdgeType } from '@/types/index.ts'; +import { EDGE_COLORS } from '@/config/index.ts'; + +const initialState: AppState = { + base: 'hst', + lang: 'es', + view: 'grid', + search: '', + group: 'all', + library: 'all', + libraryMembers: new Set(), + selectionMode: false, + selected: new Set(), + selectedTag: null, + tags: [], + hstTags: [], + groups: [], + libraries: [], + graphEdges: [], + treeEdges: [], + graphFilters: { + cats: new Set(['hst'] as const), + edges: new Set(Object.keys(EDGE_COLORS) as EdgeType[]) + }, + graphSettings: { + nodeSize: 20, + linkDist: 80, + showImg: true, + showLbl: true + } +}; + +export const store = createStore(initialState); +export { createStore } from './store.ts'; diff --git a/hst-frontend-new/src/state/store.ts b/hst-frontend-new/src/state/store.ts new file mode 100644 index 0000000..865b837 --- /dev/null +++ b/hst-frontend-new/src/state/store.ts @@ -0,0 +1,27 @@ +type Listener = (state: T, prevState: T) => void; + +export interface Store { + getState: () => Readonly; + setState: (partial: Partial) => void; + subscribe: (listener: Listener) => () => void; +} + +export function createStore(initialState: T): Store { + let state = { ...initialState }; + const listeners = new Set>(); + + return { + getState: (): Readonly => state, + + setState: (partial: Partial): void => { + const prevState = state; + state = { ...state, ...partial }; + listeners.forEach(fn => fn(state, prevState)); + }, + + subscribe: (listener: Listener): (() => void) => { + listeners.add(listener); + return () => listeners.delete(listener); + } + }; +} diff --git a/hst-frontend-new/src/styles/main.css b/hst-frontend-new/src/styles/main.css new file mode 100644 index 0000000..792fa24 --- /dev/null +++ b/hst-frontend-new/src/styles/main.css @@ -0,0 +1,575 @@ +/* === RESET & VARIABLES === */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --bg: #0a0a0f; + --bg-secondary: #12121a; + --bg-card: #1a1a24; + --border: #2a2a3a; + --text: #e0e0e0; + --text-muted: #888; + --accent: #7c8aff; + --card-width: 176px; + --card-img-height: 176px; +} + +html, body { height: 100%; overflow: hidden; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: var(--bg); + color: var(--text); +} + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: var(--bg-secondary); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; } +::-webkit-scrollbar-thumb:hover { background: #444; } + +/* === APP LAYOUT === */ +.app { display: flex; flex-direction: column; height: 100vh; } + +/* === TOPBAR === */ +.topbar { + height: 50px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 16px; + gap: 12px; +} +.topbar-left { display: flex; align-items: center; gap: 10px; } +.topbar-center { flex: 1; display: flex; justify-content: center; } +.topbar-right { display: flex; align-items: center; gap: 10px; } +.logo { font-weight: 700; font-size: 1.2em; color: var(--accent); letter-spacing: 1px; } + +/* === BUTTONS === */ +.btn { + padding: 7px 14px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + font-size: 0.8em; + font-weight: 500; + transition: all 0.15s ease; +} +.btn:hover { border-color: var(--accent); color: var(--text); } +.btn.active { background: var(--accent); border-color: var(--accent); color: #fff; } +.btn-sm { padding: 5px 10px; font-size: 0.75em; } +.sel-count { font-size: 0.7em; margin-left: 4px; opacity: 0.8; } + +.search-input { + width: 300px; + padding: 9px 14px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + font-size: 0.9em; +} +.search-input:focus { outline: none; border-color: var(--accent); } +.search-input::placeholder { color: var(--text-muted); } + +.base-buttons { display: flex; gap: 2px; background: var(--bg-card); border-radius: 6px; padding: 3px; } +.base-btn { + padding: 6px 14px; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-muted); + cursor: pointer; + font-size: 0.8em; + font-weight: 600; + transition: all 0.15s ease; +} +.base-btn:hover { color: var(--text); } +.base-btn.active { background: var(--accent); color: #fff; } + +/* === GROUPS BAR === */ +.groups-bar { + height: 44px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 16px; + gap: 8px; + overflow-x: auto; +} +.group-btn { + padding: 6px 16px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-muted); + cursor: pointer; + font-size: 0.75em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; + transition: all 0.15s ease; +} +.group-btn:hover { border-color: var(--accent); color: var(--text); } +.group-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; } + +/* === MAIN LAYOUT === */ +.main-layout { display: flex; flex: 1; overflow: hidden; } + +/* === LEFT PANEL === */ +.left-panel { + width: 84px; + background: var(--bg-secondary); + border-right: 1px solid var(--border); + overflow-y: auto; + padding: 10px 6px; + flex-shrink: 0; +} +.lib-icon { + width: 68px; + height: 68px; + margin: 6px auto; + border-radius: 10px; + background: var(--bg-card); + border: 2px solid transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + transition: all 0.15s ease; + overflow: hidden; +} +.lib-icon:hover { border-color: var(--accent); } +.lib-icon.active { border-color: var(--accent); background: rgba(124, 138, 255, 0.15); } +.lib-icon img { width: 42px; height: 42px; object-fit: cover; border-radius: 6px; } +.lib-icon span { + font-size: 0.6em; + color: var(--text-muted); + margin-top: 4px; + text-align: center; + max-width: 60px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* === CENTER PANEL === */ +.center-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; } + +/* === VIEW TABS === */ +.view-tabs { display: flex; gap: 6px; } +.view-tab { + padding: 7px 20px; + background: transparent; + border: none; + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + font-size: 0.85em; + font-weight: 500; + transition: all 0.15s ease; +} +.view-tab:hover { color: var(--text); background: var(--bg-card); } +.view-tab.active { background: var(--accent); color: #fff; } + +/* === CONTENT AREA === */ +.content-area { flex: 1; overflow: hidden; position: relative; } + +/* === GRID VIEW === */ +.grid-view { + display: flex; + flex-wrap: wrap; + align-content: flex-start; + gap: 16px; + padding: 20px; + overflow-y: auto; + height: 100%; +} + +.card { + width: var(--card-width); + flex-shrink: 0; + flex-grow: 0; + background: var(--bg-card); + border-radius: 10px; + border: 1px solid var(--border); + overflow: hidden; + cursor: pointer; + transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; + position: relative; +} +.card:hover { + border-color: var(--accent); + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0,0,0,0.3); +} +.card.selected { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(124, 138, 255, 0.4); +} + +.card-checkbox { + position: absolute; + top: 10px; + left: 10px; + width: 24px; + height: 24px; + border-radius: 6px; + background: rgba(0,0,0,0.7); + border: 2px solid var(--border); + display: none; + align-items: center; + justify-content: center; + z-index: 5; + transition: all 0.15s ease; +} +.card-checkbox.visible { display: flex; } +.card-checkbox.checked { background: var(--accent); border-color: var(--accent); } +.card-checkbox.checked::after { content: "\2713"; color: #fff; font-size: 14px; font-weight: bold; } + +.card-image { + width: var(--card-width); + height: var(--card-img-height); + background: linear-gradient(145deg, #1a1a24 0%, #0a0a0f 100%); + position: relative; + overflow: hidden; +} + +.card-placeholder { + width: var(--card-width); + height: var(--card-img-height); + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5em; + font-weight: 700; + color: var(--accent); + opacity: 0.5; + text-transform: uppercase; + background: linear-gradient(145deg, #1a1a24 0%, #0a0a0f 100%); +} + +.card-img { + width: var(--card-width); + height: var(--card-img-height); + object-fit: cover; +} + +.card-body { padding: 12px; } +.card-ref { + font-size: 0.75em; + color: var(--accent); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.card-name { + font-size: 0.85em; + color: var(--text); + margin-top: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.3; +} + +/* === TREE VIEW === */ +.tree-view { + padding: 20px; + overflow-y: auto; + height: 100%; +} +.tree-group { margin-bottom: 12px; } +.tree-header { + display: flex; + align-items: center; + padding: 10px 12px; + cursor: pointer; + border-radius: 8px; + background: var(--bg-card); + transition: background 0.15s ease; +} +.tree-header:hover { background: var(--bg-secondary); } +.tree-toggle { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 1em; + font-weight: bold; + flex-shrink: 0; +} +.tree-group-name { flex: 1; font-weight: 500; } +.tree-count { + font-size: 0.75em; + color: var(--text-muted); + background: var(--bg-secondary); + padding: 4px 10px; + border-radius: 12px; +} +.tree-items { display: none; margin-left: 28px; margin-top: 4px; } +.tree-items.expanded { display: block; } +.tree-item { + display: flex; + align-items: center; + padding: 8px 12px; + cursor: pointer; + border-radius: 6px; + margin: 3px 0; + gap: 10px; + transition: background 0.15s ease; +} +.tree-item:hover { background: var(--bg-card); } +.tree-item.selected { background: rgba(124,138,255,0.15); } +.tree-img { + width: 32px; + height: 32px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + background: var(--bg-card); +} +.tree-placeholder { + width: 32px; + height: 32px; + border-radius: 6px; + background: var(--bg-card); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9em; + font-weight: 600; + color: var(--accent); +} +.tree-name { font-size: 0.9em; } + +/* === GRAPH VIEW === */ +.graph-view { + width: 100%; + height: 100%; + position: relative; +} +.graph-view svg { width: 100%; height: 100%; display: block; background: var(--bg); } +.node { cursor: pointer; } +.node text { fill: var(--text-muted); pointer-events: none; font-size: 11px; } +.node.selected circle { stroke: var(--accent); stroke-width: 4; } +.link { stroke-opacity: 0.5; } + +/* === DETAIL PANEL === */ +.detail-panel { + width: 0; + background: var(--bg-secondary); + border-left: 1px solid var(--border); + overflow-y: auto; + overflow-x: hidden; + transition: width 0.3s ease; + flex-shrink: 0; +} +.detail-panel.open { width: 360px; } + +.detail-header { + position: relative; + width: 100%; + height: 220px; + background: linear-gradient(145deg, var(--bg-card) 0%, var(--bg) 100%); + overflow: hidden; +} +.detail-placeholder { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 5em; + font-weight: 700; + color: var(--accent); + opacity: 0.4; + text-transform: uppercase; +} +.detail-img { + width: 100%; + height: 100%; + object-fit: cover; +} +.detail-close { + position: absolute; + top: 12px; + right: 12px; + width: 36px; + height: 36px; + border-radius: 50%; + background: rgba(0,0,0,0.7); + border: none; + color: #fff; + cursor: pointer; + font-size: 20px; + z-index: 5; + transition: background 0.15s ease; +} +.detail-close:hover { background: rgba(0,0,0,0.9); } + +.detail-body { padding: 20px; } +.detail-ref { + font-size: 1.2em; + color: var(--accent); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; +} +.detail-mrf { + font-size: 0.7em; + color: var(--text-muted); + margin-top: 8px; + font-family: monospace; + word-break: break-all; + cursor: pointer; + padding: 8px 10px; + background: var(--bg-card); + border-radius: 6px; + transition: color 0.15s ease; +} +.detail-mrf:hover { color: var(--accent); } +.detail-name { font-size: 1.3em; color: var(--text); margin-top: 16px; font-weight: 500; } +.detail-desc { font-size: 0.9em; color: var(--text-muted); margin-top: 12px; line-height: 1.7; } + +.detail-section { margin-top: 24px; } +.detail-section h4 { + font-size: 0.75em; + color: var(--text-muted); + text-transform: uppercase; + margin-bottom: 12px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.chip-list { display: flex; flex-wrap: wrap; gap: 8px; } +.tag-chip { + padding: 7px 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.8em; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s ease; +} +.tag-chip:hover { border-color: var(--accent); color: var(--text); } + +/* === TOAST === */ +.toast { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + background: var(--accent); + color: #fff; + padding: 14px 28px; + border-radius: 10px; + font-size: 0.9em; + font-weight: 500; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 1000; + pointer-events: none; +} +.toast.show { opacity: 1; } + +/* === MODAL === */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.85); + display: none; + align-items: center; + justify-content: center; + z-index: 100; +} +.modal.open { display: flex; } +.modal-content { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 14px; + width: 90%; + max-width: 620px; + max-height: 80vh; + overflow: hidden; +} +.modal-header { + padding: 18px 22px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} +.modal-header h3 { font-size: 1.15em; color: var(--text); font-weight: 600; } +.modal-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1.5em; + padding: 4px 8px; +} +.modal-close:hover { color: var(--text); } +.modal-body { padding: 22px; overflow-y: auto; max-height: calc(80vh - 65px); } +.api-item { margin-bottom: 18px; } +.api-endpoint { + font-family: monospace; + font-size: 0.9em; + color: var(--accent); + background: var(--bg-card); + padding: 12px 14px; + border-radius: 8px; +} +.api-desc { font-size: 0.85em; color: var(--text-muted); margin-top: 8px; } + +/* === EMPTY STATE === */ +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + gap: 16px; + padding: 40px; +} + +/* === LOADING === */ +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + gap: 14px; + font-size: 1em; +} +.loading::after { + content: ""; + width: 28px; + height: 28px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* === SELECT === */ +select { + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg-card); + color: var(--text); + font-size: 0.8em; + cursor: pointer; +} +select:focus { outline: none; border-color: var(--accent); } diff --git a/hst-frontend-new/src/types/graph.ts b/hst-frontend-new/src/types/graph.ts new file mode 100644 index 0000000..ceaaa4b --- /dev/null +++ b/hst-frontend-new/src/types/graph.ts @@ -0,0 +1,37 @@ +export type EdgeType = + | 'relation' + | 'specialization' + | 'mirror' + | 'dependency' + | 'sequence' + | 'composition' + | 'hierarchy' + | 'library' + | 'contextual' + | 'association'; + +export type CategoryKey = 'hst' | 'spe' | 'vue' | 'vsn' | 'msn' | 'flg'; + +export interface GraphEdge { + mrf_a: string; + mrf_b: string; + edge_type: EdgeType; + weight?: number; +} + +export interface TreeEdge { + mrf_parent: string; + mrf_child: string; +} + +export interface GraphNode { + id: string; + ref: string; + name: string; + img: string; + cat: CategoryKey; + x?: number; + y?: number; + fx?: number | null; + fy?: number | null; +} diff --git a/hst-frontend-new/src/types/index.ts b/hst-frontend-new/src/types/index.ts new file mode 100644 index 0000000..5d577bd --- /dev/null +++ b/hst-frontend-new/src/types/index.ts @@ -0,0 +1,16 @@ +export type { Tag, Group, Library, ChildTag, RelatedTag } from './tag.ts'; +export type { + EdgeType, + CategoryKey, + GraphEdge, + TreeEdge, + GraphNode +} from './graph.ts'; +export type { + ViewType, + BaseType, + LangType, + GraphFilters, + GraphSettings, + AppState +} from './state.ts'; diff --git a/hst-frontend-new/src/types/state.ts b/hst-frontend-new/src/types/state.ts new file mode 100644 index 0000000..6160185 --- /dev/null +++ b/hst-frontend-new/src/types/state.ts @@ -0,0 +1,48 @@ +import type { Tag, Group, Library } from './tag.ts'; +import type { GraphEdge, TreeEdge, CategoryKey, EdgeType } from './graph.ts'; + +export type ViewType = 'grid' | 'tree' | 'graph'; +export type BaseType = 'hst' | 'flg' | 'itm' | 'loc' | 'ply'; +export type LangType = 'es' | 'en' | 'ch'; + +export interface GraphFilters { + cats: Set; + edges: Set; +} + +export interface GraphSettings { + nodeSize: number; + linkDist: number; + showImg: boolean; + showLbl: boolean; +} + +export interface AppState { + // Navigation + base: BaseType; + lang: LangType; + view: ViewType; + + // Filters + search: string; + group: string; + library: string; + libraryMembers: Set; + + // Selection + selectionMode: boolean; + selected: Set; + selectedTag: Tag | null; + + // Data + tags: Tag[]; + hstTags: Tag[]; // HST tags for group name resolution + groups: Group[]; + libraries: Library[]; + graphEdges: GraphEdge[]; + treeEdges: TreeEdge[]; + + // Graph-specific + graphFilters: GraphFilters; + graphSettings: GraphSettings; +} diff --git a/hst-frontend-new/src/types/tag.ts b/hst-frontend-new/src/types/tag.ts new file mode 100644 index 0000000..9b05421 --- /dev/null +++ b/hst-frontend-new/src/types/tag.ts @@ -0,0 +1,40 @@ +export interface Tag { + mrf: string; + ref: string; + name_es?: string; + name_en?: string; + name_ch?: string; + txt?: string; + alias?: string; + set_hst?: string; + img_url?: string; + img_thumb_url?: string; +} + +export interface Group { + mrf: string; + ref: string; + name_es?: string; + name_en?: string; +} + +export interface Library { + mrf: string; + ref?: string; + name?: string; + name_es?: string; + alias?: string; + icon_url?: string; + img_thumb_url?: string; +} + +export interface ChildTag { + mrf: string; + ref?: string; + alias?: string; + name_es?: string; +} + +export interface RelatedTag extends ChildTag { + edge_type: string; +} diff --git a/hst-frontend-new/src/utils/clipboard.ts b/hst-frontend-new/src/utils/clipboard.ts new file mode 100644 index 0000000..ab117d1 --- /dev/null +++ b/hst-frontend-new/src/utils/clipboard.ts @@ -0,0 +1,14 @@ +import { toast } from './toast.ts'; + +export async function copyToClipboard(text: string, message?: string): Promise { + 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)}...`); +} diff --git a/hst-frontend-new/src/utils/dom.ts b/hst-frontend-new/src/utils/dom.ts new file mode 100644 index 0000000..50cfa99 --- /dev/null +++ b/hst-frontend-new/src/utils/dom.ts @@ -0,0 +1,44 @@ +export const $ = ( + selector: string, + parent: ParentNode = document +): T | null => parent.querySelector(selector); + +export const $$ = ( + selector: string, + parent: ParentNode = document +): T[] => Array.from(parent.querySelectorAll(selector)); + +export function createElement( + tag: K, + attrs?: Record, + 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( + container: HTMLElement, + selector: string, + eventType: string, + handler: (event: T, target: HTMLElement) => void +): void { + container.addEventListener(eventType, (event) => { + const target = (event.target as HTMLElement).closest(selector); + if (target && container.contains(target)) { + handler(event as T, target); + } + }); +} diff --git a/hst-frontend-new/src/utils/filters.ts b/hst-frontend-new/src/utils/filters.ts new file mode 100644 index 0000000..b192126 --- /dev/null +++ b/hst-frontend-new/src/utils/filters.ts @@ -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; + 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; + }); +} diff --git a/hst-frontend-new/src/utils/i18n.ts b/hst-frontend-new/src/utils/i18n.ts new file mode 100644 index 0000000..ff45891 --- /dev/null +++ b/hst-frontend-new/src/utils/i18n.ts @@ -0,0 +1,31 @@ +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 { + const map = new Map(); + 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 { + if (!mrf) return 'Sin grupo'; + return nameMap.get(mrf) || mrf.slice(0, 8); +} + +export function getImg(tag: Tag): string { + return tag.img_thumb_url || ''; +} + +export function getFullImg(tag: Tag): string { + return tag.img_url || tag.img_thumb_url || ''; +} diff --git a/hst-frontend-new/src/utils/index.ts b/hst-frontend-new/src/utils/index.ts new file mode 100644 index 0000000..a0131b6 --- /dev/null +++ b/hst-frontend-new/src/utils/index.ts @@ -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'; diff --git a/hst-frontend-new/src/utils/toast.ts b/hst-frontend-new/src/utils/toast.ts new file mode 100644 index 0000000..e5258ee --- /dev/null +++ b/hst-frontend-new/src/utils/toast.ts @@ -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); +} diff --git a/hst-frontend-new/src/views/DetailPanel/DetailPanel.ts b/hst-frontend-new/src/views/DetailPanel/DetailPanel.ts new file mode 100644 index 0000000..70833bf --- /dev/null +++ b/hst-frontend-new/src/views/DetailPanel/DetailPanel.ts @@ -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 + ) { + super(container, store); + this.panelEl = container; + } + + async showDetail(mrf: string): Promise { + 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 { + const state = this.getState(); + const img = getFullImg(tag); + const name = getName(tag, state.lang); + + this.panelEl.innerHTML = ` +
+ ${img + ? `${tag.ref}` + : `
${tag.ref?.slice(0, 2) || 'T'}
` + } + +
+
+
${tag.ref || ''}
+
${tag.mrf}
+
${name}
+
${tag.txt || tag.alias || ''}
+ + +
+ `; + + 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(this.panelEl, '.tag-chip', 'click', (_, target) => { + const mrf = target.dataset.mrf; + if (mrf) this.showDetail(mrf); + }); + } + + private async loadRelations(mrf: string): Promise { + 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 `${label}`; + }).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 `${label}`; + }).join(''); + } + } +} diff --git a/hst-frontend-new/src/views/GraphView/GraphView.ts b/hst-frontend-new/src/views/GraphView/GraphView.ts new file mode 100644 index 0000000..b708ca9 --- /dev/null +++ b/hst-frontend-new/src/views/GraphView/GraphView.ts @@ -0,0 +1,249 @@ +import { View } from '../View.ts'; +import { filterTags, getName, getImg } from '@/utils/index.ts'; +import { fetchGraphEdges, fetchTreeEdges } from '@/api/index.ts'; +import { CATS, EDGE_COLORS } from '@/config/index.ts'; +import type { Store } from '@/state/store.ts'; +import type { AppState, GraphNode, CategoryKey, EdgeType } from '@/types/index.ts'; + +type D3Module = typeof import('d3'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type D3Selection = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type D3Simulation = any; + +export class GraphView extends View { + private d3: D3Module | null = null; + private simulation: D3Simulation | null = null; + private showDetail: (mrf: string) => void; + + constructor( + container: HTMLElement, + store: Store, + showDetail: (mrf: string) => void + ) { + super(container, store); + this.showDetail = showDetail; + } + + async mount(): Promise { + this.container.innerHTML = '
Cargando grafo...
'; + + // Lazy load D3 + if (!this.d3) { + this.d3 = await import('d3'); + } + + // Load graph data + const state = this.getState(); + if (state.graphEdges.length === 0) { + const [graphEdges, treeEdges] = await Promise.all([ + fetchGraphEdges(), + fetchTreeEdges() + ]); + this.store.setState({ graphEdges, treeEdges }); + } + + this.render(); + } + + render(): void { + if (!this.d3) return; + const d3 = this.d3; + const state = this.getState(); + + // Build nodes from filtered tags + const filtered = filterTags(state.tags, { + search: state.search, + group: state.group, + library: state.library, + libraryMembers: state.libraryMembers, + lang: state.lang + }); + + const nodeMap = new Map(); + filtered.forEach(tag => { + nodeMap.set(tag.mrf, { + id: tag.mrf, + ref: tag.alias || tag.ref || tag.mrf.slice(0, 8), + name: getName(tag, state.lang), + img: getImg(tag), + cat: 'hst' as CategoryKey + }); + }); + + // Build edges + interface GraphLink { + source: string | GraphNode; + target: string | GraphNode; + type: EdgeType; + weight: number; + } + const edges: GraphLink[] = []; + + state.graphEdges.forEach(e => { + if (nodeMap.has(e.mrf_a) && nodeMap.has(e.mrf_b)) { + if (state.graphFilters.edges.has(e.edge_type)) { + edges.push({ + source: e.mrf_a, + target: e.mrf_b, + type: e.edge_type, + weight: e.weight || 1 + }); + } + } + }); + + state.treeEdges.forEach(e => { + if (nodeMap.has(e.mrf_parent) && nodeMap.has(e.mrf_child)) { + if (state.graphFilters.edges.has('hierarchy')) { + edges.push({ + source: e.mrf_parent, + target: e.mrf_child, + type: 'hierarchy', + weight: 1 + }); + } + } + }); + + const nodes = Array.from(nodeMap.values()); + + if (nodes.length === 0) { + this.container.innerHTML = '
Sin nodos para mostrar
'; + return; + } + + // Clear and create SVG + this.container.innerHTML = ''; + const width = this.container.clientWidth; + const height = this.container.clientHeight || 600; + + const svg: D3Selection = d3.select(this.container) + .append('svg') + .attr('width', '100%') + .attr('height', '100%') + .attr('viewBox', `0 0 ${width} ${height}`); + + const g = svg.append('g'); + + // Zoom + const zoom = d3.zoom() + .scaleExtent([0.1, 4]) + .on('zoom', (event: { transform: string }) => { + g.attr('transform', event.transform); + }); + + svg.call(zoom); + + // Simulation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.simulation = d3.forceSimulation(nodes as any) + .force('link', d3.forceLink(edges) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .id((d: any) => d.id) + .distance(state.graphSettings.linkDist)) + .force('charge', d3.forceManyBody().strength(-150)) + .force('center', d3.forceCenter(width / 2, height / 2)) + .force('collision', d3.forceCollide().radius(state.graphSettings.nodeSize + 5)); + + // Links + const link = g.append('g') + .selectAll('line') + .data(edges) + .join('line') + .attr('stroke', (d: GraphLink) => EDGE_COLORS[d.type] || '#999') + .attr('stroke-width', (d: GraphLink) => Math.sqrt(d.weight)) + .attr('stroke-opacity', 0.6); + + // Nodes + const node = g.append('g') + .selectAll('g') + .data(nodes) + .join('g') + .attr('cursor', 'pointer') + .call(this.createDrag(d3, this.simulation)); + + const nodeSize = state.graphSettings.nodeSize; + + if (state.graphSettings.showImg) { + node.append('image') + .attr('xlink:href', (d: GraphNode) => d.img || '') + .attr('width', nodeSize) + .attr('height', nodeSize) + .attr('x', -nodeSize / 2) + .attr('y', -nodeSize / 2) + .attr('clip-path', 'circle(50%)'); + + // Fallback for nodes without image + node.filter((d: GraphNode) => !d.img) + .append('circle') + .attr('r', nodeSize / 2) + .attr('fill', (d: GraphNode) => CATS[d.cat]?.color || '#7c8aff'); + } else { + node.append('circle') + .attr('r', nodeSize / 2) + .attr('fill', (d: GraphNode) => CATS[d.cat]?.color || '#7c8aff'); + } + + if (state.graphSettings.showLbl) { + node.append('text') + .text((d: GraphNode) => d.ref) + .attr('dy', nodeSize / 2 + 12) + .attr('text-anchor', 'middle') + .attr('font-size', 10) + .attr('fill', 'var(--text-primary)'); + } + + node.on('click', (_: MouseEvent, d: GraphNode) => { + if (state.selectionMode) { + const newSelected = new Set(state.selected); + if (newSelected.has(d.id)) { + newSelected.delete(d.id); + } else { + newSelected.add(d.id); + } + this.store.setState({ selected: newSelected }); + } else { + this.showDetail(d.id); + } + }); + + // Tick + this.simulation.on('tick', () => { + link + .attr('x1', (d: GraphLink) => (d.source as GraphNode).x!) + .attr('y1', (d: GraphLink) => (d.source as GraphNode).y!) + .attr('x2', (d: GraphLink) => (d.target as GraphNode).x!) + .attr('y2', (d: GraphLink) => (d.target as GraphNode).y!); + + node.attr('transform', (d: GraphNode) => `translate(${d.x},${d.y})`); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private createDrag(d3: D3Module, simulation: D3Simulation): any { + return d3.drag() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .on('start', (event: any, d: any) => { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .on('drag', (event: any, d: any) => { + d.fx = event.x; + d.fy = event.y; + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .on('end', (event: any, d: any) => { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }); + } + + unmount(): void { + this.simulation?.stop(); + super.unmount(); + } +} diff --git a/hst-frontend-new/src/views/GridView/GridView.ts b/hst-frontend-new/src/views/GridView/GridView.ts new file mode 100644 index 0000000..48eef00 --- /dev/null +++ b/hst-frontend-new/src/views/GridView/GridView.ts @@ -0,0 +1,75 @@ +import { View } from '../View.ts'; +import { filterTags, getName, getImg, delegateEvent } from '@/utils/index.ts'; +import type { Store } from '@/state/store.ts'; +import type { AppState } from '@/types/index.ts'; + +export class GridView extends View { + private showDetail: (mrf: string) => void; + + constructor( + container: HTMLElement, + store: Store, + showDetail: (mrf: string) => void + ) { + super(container, store); + this.showDetail = showDetail; + } + + render(): void { + const state = this.getState(); + const filtered = filterTags(state.tags, { + search: state.search, + group: state.group, + library: state.library, + libraryMembers: state.libraryMembers, + lang: state.lang + }); + + if (filtered.length === 0) { + this.container.innerHTML = '
Sin resultados
'; + return; + } + + this.container.innerHTML = filtered.map(tag => { + const img = getImg(tag); + const name = getName(tag, state.lang); + const isSelected = state.selected.has(tag.mrf); + + return ` +
+ ${state.selectionMode ? ` + + ` : ''} + ${img + ? `${tag.ref}` + : `
${tag.ref?.slice(0, 2) || 'T'}
` + } +
${name}
+
+ `; + }).join(''); + + this.bindEvents(); + } + + private bindEvents(): void { + const state = this.getState(); + + delegateEvent(this.container, '.card', 'click', (_, target) => { + const mrf = target.dataset.mrf; + if (!mrf) return; + + if (state.selectionMode) { + const newSelected = new Set(state.selected); + if (newSelected.has(mrf)) { + newSelected.delete(mrf); + } else { + newSelected.add(mrf); + } + this.setState({ selected: newSelected }); + } else { + this.showDetail(mrf); + } + }); + } +} diff --git a/hst-frontend-new/src/views/TreeView/TreeView.ts b/hst-frontend-new/src/views/TreeView/TreeView.ts new file mode 100644 index 0000000..9581c5d --- /dev/null +++ b/hst-frontend-new/src/views/TreeView/TreeView.ts @@ -0,0 +1,109 @@ +import { View } from '../View.ts'; +import { filterTags, getName, getImg, delegateEvent, createNameMap, resolveGroupName } from '@/utils/index.ts'; +import type { Store } from '@/state/store.ts'; +import type { AppState, Tag } from '@/types/index.ts'; + +export class TreeView extends View { + private showDetail: (mrf: string) => void; + private expanded: Set = new Set(); + + constructor( + container: HTMLElement, + store: Store, + showDetail: (mrf: string) => void + ) { + super(container, store); + this.showDetail = showDetail; + } + + render(): void { + const state = this.getState(); + // Use hstTags for group name resolution (set_hst points to hst tags) + const nameMap = createNameMap(state.hstTags, state.lang); + const filtered = filterTags(state.tags, { + search: state.search, + group: state.group, + library: state.library, + libraryMembers: state.libraryMembers, + lang: state.lang + }); + + // Group by set_hst + const groups = new Map(); + filtered.forEach(tag => { + const group = tag.set_hst || 'sin-grupo'; + if (!groups.has(group)) groups.set(group, []); + groups.get(group)!.push(tag); + }); + + if (groups.size === 0) { + this.container.innerHTML = '
Sin resultados
'; + return; + } + + this.container.innerHTML = Array.from(groups.entries()).map(([groupMrf, tags]) => { + const isExpanded = this.expanded.has(groupMrf); + const groupName = resolveGroupName(groupMrf === 'sin-grupo' ? undefined : groupMrf, nameMap); + return ` +
+
+ ${isExpanded ? '−' : '+'} + ${groupName} + ${tags.length} +
+
+ ${tags.map(tag => { + const img = getImg(tag); + const name = getName(tag, state.lang); + const isSelected = state.selected.has(tag.mrf); + return ` +
+ ${img + ? `${tag.ref}` + : `
${tag.ref?.slice(0, 1) || 'T'}
` + } + ${name} +
+ `; + }).join('')} +
+
+ `; + }).join(''); + + this.bindEvents(); + } + + private bindEvents(): void { + const state = this.getState(); + + delegateEvent(this.container, '.tree-header', 'click', (_, target) => { + const group = target.dataset.group; + if (!group) return; + + if (this.expanded.has(group)) { + this.expanded.delete(group); + } else { + this.expanded.add(group); + } + this.render(); + }); + + delegateEvent(this.container, '.tree-item', 'click', (_, target) => { + const mrf = target.dataset.mrf; + if (!mrf) return; + + if (state.selectionMode) { + const newSelected = new Set(state.selected); + if (newSelected.has(mrf)) { + newSelected.delete(mrf); + } else { + newSelected.add(mrf); + } + this.setState({ selected: newSelected }); + } else { + this.showDetail(mrf); + } + }); + } +} diff --git a/hst-frontend-new/src/views/View.ts b/hst-frontend-new/src/views/View.ts new file mode 100644 index 0000000..7697010 --- /dev/null +++ b/hst-frontend-new/src/views/View.ts @@ -0,0 +1,33 @@ +import type { Store } from '@/state/store.ts'; +import type { AppState } from '@/types/index.ts'; + +export abstract class View { + protected container: HTMLElement; + protected store: Store; + protected unsubscribe?: () => void; + + constructor(container: HTMLElement, store: Store) { + this.container = container; + this.store = store; + } + + abstract render(): void; + + mount(): void { + this.unsubscribe = this.store.subscribe(() => this.render()); + this.render(); + } + + unmount(): void { + this.unsubscribe?.(); + this.container.innerHTML = ''; + } + + protected getState(): AppState { + return this.store.getState(); + } + + protected setState(partial: Partial): void { + this.store.setState(partial); + } +} diff --git a/hst-frontend-new/src/views/index.ts b/hst-frontend-new/src/views/index.ts new file mode 100644 index 0000000..46261be --- /dev/null +++ b/hst-frontend-new/src/views/index.ts @@ -0,0 +1,5 @@ +export { View } from './View.ts'; +export { GridView } from './GridView/GridView.ts'; +export { TreeView } from './TreeView/TreeView.ts'; +export { GraphView } from './GraphView/GraphView.ts'; +export { DetailPanel } from './DetailPanel/DetailPanel.ts'; diff --git a/hst-frontend-new/tsconfig.json b/hst-frontend-new/tsconfig.json new file mode 100644 index 0000000..7f3fdd5 --- /dev/null +++ b/hst-frontend-new/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@types/*": ["src/types/*"], + "@config/*": ["src/config/*"], + "@state/*": ["src/state/*"], + "@api/*": ["src/api/*"], + "@utils/*": ["src/utils/*"], + "@components/*": ["src/components/*"], + "@views/*": ["src/views/*"], + "@layout/*": ["src/layout/*"] + } + }, + "include": ["src"] +} diff --git a/hst-frontend-new/vite.config.ts b/hst-frontend-new/vite.config.ts new file mode 100644 index 0000000..c6d34f6 --- /dev/null +++ b/hst-frontend-new/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + root: '.', + base: './', + + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + '@types': resolve(__dirname, 'src/types'), + '@config': resolve(__dirname, 'src/config'), + '@state': resolve(__dirname, 'src/state'), + '@api': resolve(__dirname, 'src/api'), + '@utils': resolve(__dirname, 'src/utils'), + '@components': resolve(__dirname, 'src/components'), + '@views': resolve(__dirname, 'src/views'), + '@layout': resolve(__dirname, 'src/layout'), + } + }, + + build: { + target: 'es2020', + outDir: 'dist', + rollupOptions: { + output: { + manualChunks: { + d3: ['d3'] + } + } + } + }, + + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://72.62.1.113:3000', + changeOrigin: true + } + } + } +});