diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 3db435c6..04408fb9 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -190,8 +190,6 @@ const AppWrapper = () => { Loading...}>} /> Loading...}>} /> - - Loading...}>} /> {/* Playground Routes */} diff --git a/packages/ui/src/components/ListLayout.tsx b/packages/ui/src/components/ListLayout.tsx index ee0e9b9f..e5f9200a 100644 --- a/packages/ui/src/components/ListLayout.tsx +++ b/packages/ui/src/components/ListLayout.tsx @@ -204,6 +204,11 @@ export const ListLayout = ({ return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, feedPosts, isMobile]); + // Reset selection when category changes (must run before auto-select below, or it clears the first item) + useEffect(() => { + setSelectedId(null); + }, [categorySlugs?.join(',')]); + // Select first item by default on desktop if nothing selected useEffect(() => { if (!isMobile && !selectedId && feedPosts.length > 0) { @@ -211,11 +216,6 @@ export const ListLayout = ({ } }, [feedPosts, isMobile, selectedId]); - // Reset selection when category changes - useEffect(() => { - setSelectedId(null); - }, [categorySlugs?.join(',')]); - if (loading && feedPosts.length === 0) { return
Loading...
; } @@ -278,10 +278,12 @@ export const ListLayout = ({ if (!isMobile) { // Desktop Split Layout return ( -
- {/* Left: List */} -
-
+
+ {/* Left: list scrolls independently from detail pane */} +
+
{renderItems(false)} {hasMore && (
@@ -291,8 +293,8 @@ export const ListLayout = ({
- {/* Right: Detail */} -
+ {/* Right: detail scrolls independently (post / page / place handle their own overflow) */} +
{selectedId ? ( (() => { const selectedPost = feedPosts.find((p: any) => p.id === selectedId); @@ -343,7 +345,7 @@ export const ListLayout = ({ key={selectedId} postId={selectedId} embedded - className="h-[inherit] overflow-y-auto scrollbar-custom" + className="h-full min-h-0 overflow-hidden flex flex-col" /> ); diff --git a/packages/ui/src/components/widgets/CategoryFeedWidget.tsx b/packages/ui/src/components/widgets/CategoryFeedWidget.tsx index 1bf72397..9c2bca1f 100644 --- a/packages/ui/src/components/widgets/CategoryFeedWidget.tsx +++ b/packages/ui/src/components/widgets/CategoryFeedWidget.tsx @@ -65,6 +65,7 @@ const CategoryFeedWidget: React.FC = ({ showSocial, variables, searchQuery, + fillHeight, ...rest }) => { const appConfig = useAppConfig(); @@ -210,6 +211,7 @@ const CategoryFeedWidget: React.FC = ({ initialContentType={filterType} heading={heading} headingLevel={headingLevel} + fillHeight={fillHeight} />
); diff --git a/packages/ui/src/components/widgets/HomeWidget.tsx b/packages/ui/src/components/widgets/HomeWidget.tsx index 2927fd4b..e3178273 100644 --- a/packages/ui/src/components/widgets/HomeWidget.tsx +++ b/packages/ui/src/components/widgets/HomeWidget.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation, Link } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import type { FeedSortOption } from '@/hooks/useFeedData'; import { useMediaRefresh } from '@/contexts/MediaRefreshContext'; @@ -57,6 +57,12 @@ export interface HomeWidgetProps { /** Widget editor: composite widgets use this; HomeWidget ignores it */ isEditMode?: boolean; onPropsChange?: (props: Record) => void; + /** + * When true, the widget fills the parent height (`h-full`) and uses flex + `min-h-0` so list/grid + * layouts scroll inside the widget instead of stretching the page. Use when embedding in CMS pages + * or any container with a defined height. + */ + fillHeight?: boolean; } const SIDEBAR_KEY = 'categorySidebarSize'; @@ -86,6 +92,7 @@ const HomeWidget: React.FC = ({ initialVisibilityFilter, isEditMode: _isEditMode, onPropsChange: _onPropsChange, + fillHeight = false, }) => { const { refreshKey } = useMediaRefresh(); const isMobile = useIsMobile(); @@ -174,6 +181,15 @@ const HomeWidget: React.FC = ({ return propCategorySlugs.split(',').map(s => s.trim()).filter(Boolean); }, [propCategorySlugs]); + /** Same targets as {@link CategoryTreeView} category selection: `/categories/…` or home. */ + const headingLinkTo = useMemo( + () => + categorySlugs && categorySlugs.length > 0 + ? `/categories/${categorySlugs.join('/')}` + : '/', + [categorySlugs], + ); + const cardPreset = useMemo( () => ({ showTitle, @@ -403,8 +419,25 @@ const HomeWidget: React.FC = ({ return ; }; + // Fixed height (not only min-height) so children can use h-full / % and grid min-h-0 works. + const listDesktopShellClass = cn( + 'flex flex-col overflow-hidden', + fillHeight ? 'min-h-0 flex-1' : 'h-[calc(100vh-8rem)] min-h-0', + ); + // With categories: toolbar sits above the panel group — use flex-1 inside listDesktopShellClass, not a second full-viewport height. + const feedPanelGroupClass = cn( + 'flex flex-col overflow-hidden', + fillHeight || viewMode === 'list' ? 'min-h-0 flex-1' : 'min-h-[calc(100vh-8rem)]', + ); + return ( -
+
{/* Mobile: Sheet for category navigation */} @@ -422,15 +455,26 @@ const HomeWidget: React.FC = ({ )} -
+
{isMobile ? ( /* ---- Mobile layout ---- */ -
+
{heading && (() => { const H = headingLevel || 'h2'; - return {heading}; + return ( +
+ + + {heading} + + +
+ ); })()} {showSortBar && ( @@ -450,11 +494,13 @@ const HomeWidget: React.FC = ({ )}
+ {categorySlugs && categorySlugs.length > 0 && ( - {categorySlugs.map(s => s.replace(/-/g, ' ')).join(', ')} + )} +
{showLayoutToggles && ( v && setViewMode(v as any)}> @@ -531,13 +577,28 @@ const HomeWidget: React.FC = ({
) : showCategories ? ( /* ---- Desktop with category sidebar ---- */ -
-
+
+
{heading && (() => { const H = headingLevel || 'h2'; const cls = H === 'h1' ? 'text-2xl' : H === 'h2' ? 'text-xl' : H === 'h3' ? 'text-lg' : 'text-base'; - return {heading}; + return ( + + + {heading} + + + ); })()} {showSortBar && renderSortBar()}
@@ -556,7 +617,7 @@ const HomeWidget: React.FC = ({ )}
- + = ({
- - {renderFeed()} + +
+ {renderFeed()} +
) : ( /* ---- Desktop without sidebar ---- */ -
-
+
+
{heading && (() => { const H = headingLevel || 'h2'; const cls = H === 'h1' ? 'text-2xl' : H === 'h2' ? 'text-xl' : H === 'h3' ? 'text-lg' : 'text-base'; - return {heading}; + return ( + + + {heading} + + + ); })()} {showSortBar && renderSortBar()}
@@ -602,7 +684,14 @@ const HomeWidget: React.FC = ({ )}
- {renderFeed()} +
+ {renderFeed()} +
)}
diff --git a/packages/ui/src/i18n/es.json b/packages/ui/src/i18n/es.json index 27cc2913..b84706dd 100644 --- a/packages/ui/src/i18n/es.json +++ b/packages/ui/src/i18n/es.json @@ -1,28 +1,87 @@ { + "10001": "10001", + "(stored securely)": "(guardado en lugar seguro)", "# Common Features\n\n* Supported protocols: ModbusTCP, Serial, WebSocket and REST\n* Modern web interface with built-in HMI designer\n* Many safety features for fault detection, overload and overheat\n* Supported: Omron PIDs and VFDs, Delta VFD, Sako VFD\n\n### Features Sheetpress\n\n* Sequential Heating to deal with power supply limits\n* Supports up to 32 PID controllers, Omron E5x - via Modbus-RTU\n* Temperature & Pressure Profiles\n* Adaptive Pressure\n* Cost monitoring\n\n### Features Injection Machine\n\n* Temperature & Pressure Profiles\n* Adaptive Pressure / Post flow\n\n### Features Extrusion Machine\n\n* Temperature & Pressure Profiles\n* Adaptive Pressure / Post flow\n\n## Resources\n\n* [Knowledgebase](https://polymech.info/en/resources/cassandra/home/)\n* [Modbus Interface](https://polymech.info/en/resources/cassandra/modbus/)\n* [Cassandra Firmware source](https://git.polymech.info/polymech/firmware-base)\n* [Cassandra Firmware documentation - DeepWiki](https://deepwiki.com/polymech-info/firmware/1-overview)": "# Características comunes\n\n* Protocolos compatibles: ModbusTCP, serie, WebSocket y REST\n* Moderna interfaz web con diseñador de HMI integrado\n* Numerosas funciones de seguridad para detección de fallos, sobrecarga y sobrecalentamiento\n* Compatible: PIDs y VFDs Omron, Delta VFD, Sako VFD\n\n### Características Sheetpress\n\n* Calentamiento secuencial para hacer frente a los límites de alimentación\n* Admite hasta 32 controladores PID, Omron E5x - a través de Modbus-RTU\n* Perfiles de temperatura y presión\n* Presión adaptativa\n* Control de costes\n\n### Características Máquina de inyección\n\n* Perfiles de temperatura y presión\n* Presión adaptativa / Postflujo\n\n### Características Máquina de extrusión\n\n* Perfiles de temperatura y presión\n* Presión adaptativa / Postflujo\n\n## Recursos\n\n* [Base de conocimientos](https://polymech.info/en/resources/cassandra/home/)\n* [Interfaz Modbus](https://polymech.info/en/resources/cassandra/modbus/)\n* [Fuente del firmware Cassandra](https://git.polymech.info/polymech/firmware-base)\n* [Documentación del Firmware Cassandra - DeepWiki](https://deepwiki.com/polymech-info/firmware/1-overview)\n", "# Vintage Delux Specifications\n\n| Specification | Value |\n| ---------------------- | ------------------------------------------------------ |\n| Variant | Vintage Delux |\n| Weight | 27 Kg |\n| Package Dimensions | 70 x 45 x 45 cm |\n| Plunger Diameter | 25 mm |\n| Usable Barrel Length | Up to 45 cm |\n| Supported Voltage | 220V / 110V |\n| Input Flake Size | Small - Medium |\n| Transmission | 1 : 1 |\n| Mold Interface | M20 thread / Cone Nozzle |\n| Heatbands | 3 (250W) / Ceramic |\n| Temperature Controller | 2 – RexC-100 |\n| Shotsize | 145g |\n| Features | Shutoff Valve, Adjustable Mould Height, Quick Clamping |": "# Especificaciones Vintage Delux\n\n| Especificación | Valor |\n| ------------------------------ | -------------------------------------------------------------- |\n| Variante | Vintage Delux |\n| Peso | 27 Kg |\n| Dimensiones del paquete | 70 x 45 x 45 cm |\n| Diámetro del émbolo | 25 mm |\n| Longitud útil del cañón | Hasta 45 cm |\n| Tensión soportada | 220V / 110V |\n| Tamaño de la escama de entrada | Pequeño - Mediano |\n| Transmisión | 1 : 1 |\n| Interfaz de molde | Rosca M20 / Boquilla cónica |\n| Bandas de calor | 3 (250W) / Cerámica |\n| Controlador de temperatura | 2 - RexC-100 |\n| Tamaño del disparo | 145g |\n| Características | Válvula de cierre, altura del molde ajustable, sujeción rápida |\n", "## Lydia – Plastic Extruder Printhead (SJ25)\n\nHigh\\-throughput, modular pellet extruder for robotic and gantry 3D printing. Engineered for clean melt flow, wide material compatibility, and fast integration.\n\n- Compact head for precise, consistent deposition\n- Quick\\-swap nozzles and heater zones; adaptive temperature and pressure control\n- Integrates with 6\\-axis robots and open CNC/PLC controls\n- Processes virgin and recycled pellets/flakes reliably\n- Visual/audio feedback and web\\-configurable parameters\n- Robust, service\\-friendly design for industrial duty\n- Ideal for large\\-format prototypes, tooling, and end\\-use parts\n\nContact us for detailed specs, nozzle sizes, and integration kits.": "## Lydia - Cabezal de impresión de extrusión de plástico (SJ25)\n\nExtrusora de pelets modular de alto rendimiento para impresión 3D robótica y de pórtico. Diseñada para un flujo de fusión limpio, una amplia compatibilidad de materiales y una integración rápida.\n\n* Cabezal compacto para una deposición precisa y uniforme\n* Boquillas y zonas de calentamiento intercambiables; control adaptativo de la temperatura y la presión\n* Se integra con robots de 6 ejes y controles CNC/PLC abiertos\n* Procesa de forma fiable pelets y copos vírgenes y reciclados\n* Información visual y sonora y parámetros configurables desde la web\n* Diseño robusto y de fácil mantenimiento para uso industrial\n* Ideal para prototipos de gran formato, utillaje y piezas de uso final\n\nPóngase en contacto con nosotros para obtener especificaciones detalladas, tamaños de boquillas y kits de integración.\n", + "+1 555 000 0000": "+1 555 000 0000", + "+1 555 123 4567": "+1 555 123 4567", "| **Name** | Lydia - Printhead |\n| --- | --- |\n| **Version** | 3.5 |\n| **Weight** | 35KG - 60KG |\n| **Package Dimensions** | 80 x 80 x 300 cm |\n| **Screw Diameter** | 25 - 35 mm |\n| **Barrel Diameter** | 40mm - 55mm |\n| **Barrel Length** | 42cm - 70cm |\n| **Voltage** | 220V - 380V |\n| **Gearbox** | 1:30 |\n| **Supported VFDs** | Delta, ABB, Omron & Sako |\n| **Supported Robots** | ABB & Kuga |\n| **Communication** | Modbus-RTU/TCP / REST / Websocket |\n| **Motor Power** | 1.5 - 3kW |\n| **Input Flake Size** | Small - Medium |\n": "| **Nombre** | Lydia - Cabezal de impresión |\n| ---------------------------------- | --------------------------------- |\n| **Versión** | 3.5 |\n| **Peso** | 35KG - 60KG |\n| **Dimensiones del paquete** | 80 x 80 x 300 cm |\n| **Diámetro del tornillo** | 25 - 35 mm |\n| **Diámetro del cañón** | 40 mm - 55 mm |\n| **Longitud del cañón** | 42cm - 70cm |\n| **Tensión** | 220V - 380V |\n| **Caja de cambios** | 1:30 |\n| **VFD compatibles** | Delta, ABB, Omron y Sako |\n| **Robots compatibles** | ABB y Kuga |\n| **Comunicación** | Modbus-RTU/TCP / REST / Websocket |\n| **Potencia del motor** | 1,5 - 3 kW |\n| **Tamaño de la escama de entrada** | Pequeño - Mediano |\n", "| Name | PolyMech Controller – EDC |\n|------|----------------------------|\n| Protocols | Modbus-RTU / ModbusTCP / Serial / WebSocket / REST |\n| Processor | ESP-32-S3 |\n| Board | Waveshare-6-Channel |\n| Connectivity | Wifi / Bluetooth / USB-C / RS485 |\n| Supported VFDs | Omron, ABB, Delta, Hynang, Sako |\n| Supported Temperature Controllers | Omron-E5x |\n| GPIO | 6 Digital / 2 PWM / 2 Analog In |\n| Status | Mature |\n| Version | V1.0 (Revision A) |\n| License | [CERN OHL v2](https://ohwr.org/cern_ohl_s_v2.txt) |\n| Weight | 25 Kg |\n": "| Nombre | Controlador PolyMech - EDC |\n| ---------------------------------------- | ------------------------------------------------- |\n| Protocolos | Modbus-RTU / ModbusTCP / Serie / WebSocket / REST |\n| Procesador | ESP-32-S3 |\n| Junta | Waveshare-6-Canales |\n| Conectividad | Wifi / Bluetooth / USB-C / RS485 |\n| VFD compatibles | Omron, ABB, Delta, Hynang, Sako |\n| Controladores de temperatura compatibles | Omron-E5x |\n| GPIO | 6 Entradas digitales / 2 PWM / 2 analógicas |\n| Estado | Maduro |\n| Versión | V1.0 (Revisión A) |\n| Licencia | [CERN OHL v2](https://ohwr.org/cern_ohl_s_v2.txt) |\n| Peso | 25 Kg |\n", + "0% means centroids must be at least 1 cell-size apart. 100% allows exact duplicates.": "0% significa que los centroides deben estar separados por al menos 1 tamaño de celda. 100% permite duplicados exactos.", + "1. Command line — one file": "1. Línea de comandos - un archivo", + "1. Parquet-Powered Flat Database": "1. Base de datos plana basada en parquet", + "1. System Overview": "1. Visión general del sistema", + "123 Main St": "123 Calle Mayor", + "123 Warehouse Blvd": "123 Warehouse Blvd", + "2. Aggressive Geometry Simplification & Cascading Caches": "2. Simplificación geométrica agresiva y cachés en cascada", + "2. Build & Artifact Layout": "2. Construcción y diseño de artefactos", + "2. Fit modes — not every resize is “shrink to fit”": "2. Modos de ajuste: no todos los cambios de tamaño son \"encoger para ajustar\".", + "2.1 Node.js App Build": "2.1 Creación de aplicaciones Node.js", + "2.2 Client App Build": "2.2 Creación de la aplicación cliente", + "3. Batch paths and folders": "3. Rutas y carpetas de lotes", + "3. Deployment Workflow (End-to-End)": "3. Flujo de trabajo de implantación (de extremo a extremo)", + "3. Native GeoTIFF Data Enrichment": "3. Enriquecimiento de datos GeoTIFF nativos", + "3.1 High-Level Flow": "3.1 Flujo de alto nivel", + "3.2 Example CI/CD Pipeline (Conceptual)": "3.2 Ejemplo de proceso CI/CD (conceptual)", + "3.3 Environment Management": "3.3 Gestión medioambiental", + "4. HTTP API": "4. API HTTP", + "4. PostgreSQL with Multiple Schemas": "4. PostgreSQL con múltiples esquemas", + "4.1 Why Multiple Schemas?": "4.1 ¿Por qué varios esquemas?", + "4.2 Connection & Search Path": "4.2 Conexión y ruta de búsqueda", + "4.3 Migration Strategy": "4.3 Estrategia de migración", + "4.4 Backups & Observability": "4.4 Copias de seguridad y observabilidad", + "5. Deployment Options & Trade-offs": "5. Opciones de despliegue y compensaciones", + "5. Windows — resize from Explorer": "5. Windows - redimensionar desde el Explorador", + "5.1 Patterns for Node.js App": "5.1 Patrones para aplicaciones Node.js", + "5.2 Patterns for Client App": "5.2 Modelos de aplicación cliente", + "6. Common Limitations & Pitfalls": "6. Limitaciones y dificultades habituales", + "6. Optional: Windows GUI": "6. Opcional: GUI de Windows", + "6.1 Native Node Modules": "6.1 Módulos de nodos nativos", + "6.2 File Storage (assets / storage)": "6.2 Almacenamiento de archivos (activos / almacenamiento)", + "6.3 Environment Configuration": "6.3 Configuración del entorno", + "6.4 Connection Management (Postgres)": "6.4 Gestión de conexiones (Postgres)", + "6.5 Zero-Downtime Deployments": "6.5 Despliegues sin tiempo de inactividad", + "7. Provider Comparison (Node.js + Postgres + Static Client)": "7. Comparación de proveedores (Node.js + Postgres + Cliente estático)", + "7.1 Summary Table": "7.1 Cuadro recapitulativo", + "7.2 Brief Provider Notes": "7.2 Breves notas para el proveedor", + "8. Suggested Practical Setup": "8. Configuración práctica sugerida", + "8.1 Example: Render-based Setup": "8.1 Ejemplo: Configuración basada en el renderizado", + "8.2 Example: DigitalOcean + S3-Compatible Storage": "8.2 Ejemplo: DigitalOcean + Almacenamiento compatible con S3", + "9. Checklist for Production Readiness": "9. Lista de control para la preparación de la producción", "A widget that contains its own independent layout canvas.": "Un widget que contiene su propio lienzo de diseño independiente.", "accel": "accel", + "Access and Content caches have been cleared.": "Se han borrado las cachés de Acceso y Contenido.", + "Access Control": "Control de acceso", "Access Point (AP) Mode": "Modo de punto de acceso (AP)", "Accessible only via direct link.": "Accesible sólo mediante enlace directo.", + "ACL": "ACL", + "Acme Corp": "Acme Corp", "Actions": "Acciones", + "active": "activo", + "Active Permissions": "Permisos activos", + "Add": "Añadir", "Add a comment...": "Añade un comentario...", + "Add a container to start building your layout": "Añade un contenedor para empezar a construir tu diseño", "Add a set of sample control points to this plot": "Añadir un conjunto de puntos de control de muestra a este gráfico", + "Add Address": "Añadir dirección", "Add all": "Añadir todo", "Add Button": "Botón Añadir", "Add Child": "Añadir niño", "Add Container": "Añadir contenedor", + "Add Link": "Añadir enlace", "Add rich text content with Markdown support": "Añadir contenido de texto enriquecido compatible con Markdown", "Add Samples": "Añadir muestras", "Add Slave": "Añadir esclavo", + "Add tab to left side": "Añadir pestaña a la izquierda", "Add to Cart": "Añadir a la cesta", "Add to Category": "Añadir a la categoría", + "Add Vendor Profile": "Añadir perfil de proveedor", "Add Widget": "Añadir widget", + "Add:": "Añade:", "Added to category": "Añadido a la categoría", "Addr:": "Dirección", + "Address": "Dirección", "Address Picker": "Selector de direcciones", "Admin": "Admin", "Advanced": "Avanzado", @@ -31,22 +90,38 @@ "AI Chat": "Chat AI", "AI Image Generator": "Generador de imágenes AI", "AI Layout": "Diseño AI", + "AIMLAPI API Key": "Clave API AIMLAPI", "All": "Todos", + "All groups": "Todos los grupos", "All Page Translations": "Todas las traducciones de páginas", + "All statuses": "Todos los estados", "All Stop": "Todos Stop", + "All Types": "Todos los tipos", + "Allow Missing Data Gaps": "Permitir la falta de datos", "AP Gateway": "Pasarela AP", "AP IP Address": "Dirección IP AP", "AP Password": "Contraseña AP", "AP SSID": "AP SSID", "AP Subnet Mask": "Máscara de subred AP", + "API Error on /api/vfs/ls/home/assets: Internal Server Error": "Error de API en /api/vfs/ls/home/assets: Error interno del servidor", + "API Error on /api/vfs/ls/images: Forbidden": "Error de API en /api/vfs/ls/images: Prohibido", + "API Error on /api/vfs/ls/root: Forbidden": "Error de API en /api/vfs/ls/root: Prohibido", + "API Error on /api/vfs/ls/software: Forbidden": "Error de API en /api/vfs/ls/software: Prohibido", "API Keys": "Claves API", "API URL": "URL API", + "Application ideas (with notes)": "Ideas de aplicación (con notas)", "Apply": "Solicitar", "Arbor injection machine that provides fast, repeatable, comfortable, safe and precise injection of plastic!\n\n#### \"Highlights and details\"\n\n- All parts are precision manufactured, using modern CNC and manual machines\n- Smooth and precise plunging experience\n- The plunger has a replaceable bronze tip\n- 2 mold interfaces: cone for press, and M20 thread interface\n- Heat-shield and insulation\n- Step-less mould height adjustment\n- Mould guide pins and slots\n- Transmission 1:1\n- Shot size: 145G\n- Quick Clamping\n- Shutoff valve\n- Mostly Stainless, Aluminum\n\n#### Services\n\n- After - Sales Service\n- 3 years Warranty\n- Cheap replacements for consumables\n- Customization to user needs\n- Mold design and fabrication\n\n": "Inyectora Arbor que proporciona una inyección de plástico rápida, repetible, cómoda, segura y precisa.\n\n#### \"Aspectos destacados y detalles\"\n\n* Todas las piezas se fabrican con precisión, utilizando modernas máquinas CNC y manuales\n* Experiencia de inmersión suave y precisa\n* El émbolo tiene una punta de bronce reemplazable\n* 2 interfaces de molde: cono para prensa e interfaz de rosca M20\n* Escudo térmico y aislamiento\n* Ajuste de la altura del molde sin escalones\n* Pasadores y ranuras de guía del molde\n* Transmisión 1:1\n* Tamaño del disparo: 145G\n* Sujeción rápida\n* Válvula de cierre\n* Principalmente inoxidable, aluminio\n\n#### Servicios\n\n* Servicio postventa\n* 3 años de garantía\n* Sustitución barata de consumibles\n* Adaptación a las necesidades del usuario\n* Diseño y fabricación de moldes\n", + "Are you sure you want to delete": "¿Estás seguro de que quieres borrar", + "Area Selector": "Selector de zona", + "Areas Scanned:": "Áreas escaneadas:", "Argument 0:": "Argumento 0:", "Argument 1:": "Argumento 1:", "Argument 2 (Optional):": "Argumento 2 (opcional):", "Arguments:": "Argumentos:", + "Assembly and connection tips": "Consejos de montaje y conexión", + "Assigned ACL Groups": "Grupos ACL asignados", + "Assigned types": "Tipos asignados", "Associated Controllers:": "Controladores asociados:", "Associated Signal Plot (Optional)": "Gráfico de señales asociadas (opcional)", "AUTO": "AUTO", @@ -55,7 +130,11 @@ "AUTO_MULTI": "AUTO_MULTI", "AUTO_MULTI_BALANCED": "AUTO_MULTI_BALANCED", "AUTO_TIMEOUT": "AUTO_TIMEOUT", + "Auto-send": "Envío automático", + "Automatically fetch the website and look up emails for each found location. Select this for deeper data collection (uses more credits).": "Obtener automáticamente el sitio web y buscar correos electrónicos para cada ubicación encontrada. Seleccione esta opción para obtener más datos (utiliza más créditos).", "Aux": "Aux", + "Available Types": "Tipos disponibles", + "Back": "Volver", "Back to feed": "Volver a la alimentación", "Back to Home": "Volver al inicio", "Background Video": "Vídeo de fondo", @@ -66,20 +145,41 @@ "Batch Page Translation": "Traducción de páginas por lotes", "Be the first to comment!": "¡Sé el primero en comentar!", "Be the first to like this": "Sé el primero en que te guste", + "Best Budget Options ": "Las mejores opciones económicas", + "Best Production Offers": "Mejores ofertas de producción", "Bio": "Bio", + "blocked": "bloqueado", + "Boolean": "Booleano", + "bounced": "rebotado", + "Bria API Key": "Clave API de Bria", "Browse": "Visite", + "Browse Files": "Examinar archivos", "Browse files and directories on VFS mounts": "Examinar archivos y directorios en montajes VFS", + "Browse JSON": "Examinar JSON", + "Built:": "Construido:", + "business,shop...": "negocio, tienda...", + "Button Label...": "Botón Etiqueta...", "Buzzer": "Zumbador", "Buzzer: Fast Blink": "Zumbador: Parpadeo rápido", "Buzzer: Long Beep/Short Pause": "Zumbador: Pitido largo/Pausa corta", "Buzzer: Off": "Timbre: Apagado", "Buzzer: Slow Blink": "Zumbador: Parpadeo lento", "Buzzer: Solid On": "Timbre: Encendido", + "Bypass All Filters": "Anular todos los filtros", + "Cache Control": "Control de caché", + "Cache flushed successfully": "Caché vaciada correctamente", + "Calculating...": "Calculando...", "Call Function": "Función de llamada", "Call Method": "Método de llamada", "Call REST API": "Llamar a la API REST", "Call-to-action buttons with page links": "Botones de llamada a la acción con enlaces a páginas", + "Campaign name": "Nombre de la campaña", + "Campaign saved": "Campaña guardada", + "campaigns": "campañas", + "Campaigns": "Campañas", + "Can I resize images from URLs?": "¿Puedo cambiar el tamaño de las imágenes a partir de URL?", "Cancel": "Cancelar", + "Características": "Características", "Carina": "Carina", "Cart": "Carrito", "Cassandra": "Cassandra", @@ -89,53 +189,86 @@ "Categories": "Categorías", "Category": "Categoría", "Category Feed": "Categoría Alimentación", + "Category Filter": "Categoría Filtro", "Category Hierarchy": "Jerarquía de categorías", "Category ID": "Categoría ID", "Category Manager": "Responsable de categoría", "CE": "CE", + "Cell:": "Celda:", + "cells": "células", "Center Content": "Contenido central", "Center the widget content with a maximum width container": "Centrar el contenido del widget con un contenedor de anchura máxima", + "Centroid Overlap Allowed %": "Solapamiento del centroide permitido", "Cetus": "Cetus", "Change Avatar": "Cambiar avatar", + "Change Level": "Nivel de cambio", + "Change Password": "Cambiar contraseña", + "Change Picture": "Cambiar imagen", "Change Poster": "Cartel del cambio", + "Change source": "Fuente del cambio", "Change Video": "Cambiar el vídeo", "Charts": "Gráficos", + "Chat": "Chat", + "Chat Buttons": "Botones de chat", "Chat Logs": "Registros de chat", "Child Profiles (Sub-plots)": "Perfiles de los niños (subtramas)", "Choose a picture from your published images": "Elija una imagen de las publicadas", + "Choose File": "Elegir archivo", "Choose Files": "Elegir archivos", + "Choose from the options below to start generating or uploading content.": "Elija una de las opciones siguientes para empezar a generar o cargar contenidos.", + "City": "Ciudad", "Clear": "Claro", "Clear All": "Borrar todo", "Clear All CPs": "Borrar todos los CP", "Clear category": "Borrar categoría", "Clear Chart": "Gráfico claro", + "Clear Grid": "Rejilla transparente", + "Clear Index": "Índice claro", "Clear user filter": "Borrar filtro de usuario", + "Clears the server-side content cache (memory) and the disk-based image cache. Use this if content is not updating correctly.": "Borra la caché de contenidos del servidor (memoria) y la caché de imágenes del disco. Utilícelo si el contenido no se actualiza correctamente.", + "CLI examples": "Ejemplos CLI", "Click \"Add Container\" to start building your layout": "Haz clic en \"Añadir contenedor\" para empezar a crear tu diseño", "Click \"Add Widget\" to start building your HMI": "Haga clic en \"Añadir Widget\" para empezar a crear su HMI", "Click \"Pick Video\" to select a background video": "Haz clic en \"Elegir vídeo\" para seleccionar un vídeo de fondo", + "Click anywhere on the map to inspect administrative areas, or use the search below.": "Haga clic en cualquier lugar del mapa para inspeccionar las zonas administrativas, o utilice la búsqueda que aparece a continuación.", + "Click to copy": "Haga clic para copiar", + "Click to see all emails": "Haga clic para ver todos los correos electrónicos", + "Click types in the top bar to add variables": "Haga clic en tipos en la barra superior para añadir variables", "Clipboard": "Portapapeles", "Close": "Cerrar", + "Close Tools": "Cerrar Herramientas", "Coil to Write:": "Bobina para escribir:", "Coils": "Bobinas", + "Collapse": "Colapso", "Collections": "Colecciones", "Color": "Color", "COM Write": "COM Escribir", "Coma B": "Coma B", + "Comma-separated category slugs to filter by (leave empty for all)": "Categorías separadas por comas por las que filtrar (dejar vacío para todas)", "Commons": "Comunes", + "Competitors Map": "Mapa de la competencia", + "Complete": "Complete", "Components": "Componentes", + "Condiciones": "Condiciones", "Configure": "Configure", "Configure the new control point. Press Enter to confirm or Esc to cancel.": "Configure el nuevo punto de control. Pulse Intro para confirmar o Esc para cancelar.", "Configure the series to be displayed on the chart.": "Configure las series que se mostrarán en el gráfico.", + "Configure your grid granularity and start the scan.": "Configure la granularidad de su cuadrícula e inicie la exploración.", "Connect": "Conectar", "Connect to a Modbus server to see controller data.": "Conectarse a un servidor Modbus para ver los datos del controlador.", "Connect to view register data.": "Conéctate para ver los datos del registro.", "Connected, but no register data received yet. Waiting for data...": "Conectado, pero aún no se han recibido datos de registro. Esperando datos...", "Connects to an existing Wi-Fi network.": "Se conecta a una red Wi-Fi existente.", + "Constrain process sequentially per boundary": "Limitar el proceso secuencialmente por frontera", + "Contacto (particular)": "Contacto (particular)", + "Contacts": "Contactos", + "contacts were added successfully.": "los contactos se han añadido correctamente.", "Container": "Contenedor", "Container added": "Contenedor añadido", "Containers": "Contenedores", "Content": "Contenido", "Content saved!": "¡Contenido guardado!", + "Content type": "Tipo de contenido", "Content Type": "Tipo de contenido", "Continue": "Continúe en", "Control Points": "Puntos de control", @@ -147,19 +280,31 @@ "Copy": "Copia", "Copy \"{plotName}\" to...": "Copiar \"{plotName}\" a...", "Copy \"{profileName}\" to...": "Copiar \"{nombredeperfil}\" a...", + "Copy (F5)": "Copiar (F5)", "Copy payload JSON": "Copiar carga útil JSON", + "Copy settings to clipboard": "Copiar la configuración al portapapeles", "Copy this plot to another slot...": "Copiar esta parcela a otra ranura...", + "Copy To": "Copiar a", "Copy to existing slot...": "Copiar en ranura existente...", "Copy to...": "Copiar a...", + "Copy Waypoints (GeoJSON)": "Copiar Waypoints (GeoJSON)", "Copy...": "Copia...", + "Core components": "Componentes básicos", "Corona": "Corona", "Corvus": "Corvus", + "Cost and reuse": "Coste y reutilización", + "Cost per use": "Coste por uso", + "Cost Units": "Unidades de coste", + "Country": "País", "CP Description (Optional):": "Descripción del CP (opcional):", "CP Name (Optional):": "Nombre del CP (opcional):", "Crater": "Cráter", + "Create a Page From Scratch": "Crear una página desde cero", "Create Control Point": "Crear punto de control", "Create New Control Point": "Crear nuevo punto de control", + "Create New Group...": "Crear nuevo grupo...", "Create Post": "Crear puesto", + "Create Product": "Crear producto", "Creates its own Wi-Fi network.": "Crea su propia red Wi-Fi.", "Crux": "Crux", "CSV": "CSV", @@ -169,10 +314,21 @@ "Current Status": "Situación actual", "Custom Widgets": "Widgets personalizados", "Dashboard": "Cuadro de mandos", + "data": "datos", "Data": "Datos", + "Database Synchronization": "Sincronización de bases de datos", + "Datos principales": "Datos principales", + "DE": "DE", + "DE123456789": "DE123456789", + "DE123456789000": "DE123456789000", + "Debug": "Depurar", "DEC": "DEC", "decel": "decel", + "Default": "Por defecto", + "Default Cost Units": "Unidades de coste por defecto", "Default display mode for the feed": "Modo de visualización por defecto de la alimentación", + "Default Rate Limit": "Límite de tarifa por defecto", + "Default Rate Window Time": "Hora de la ventana de tarifa por defecto", "Default sort order for the feed": "Orden por defecto para el feed", "Delete": "Borrar", "Delete All Translations": "Borrar todas las traducciones", @@ -180,15 +336,23 @@ "Delete Profile": "Borrar perfil", "Delta Vfd[15]": "Delta Vfd[15]", "Describe the image you want to create or edit... (Ctrl+V to paste images)": "Describe la imagen que quieres crear o editar... (Ctrl+V para pegar imágenes)", + "Describe the page you want the AI to create. It can generate text and images for you.": "Describe la página que quieres que cree la IA. Puede generar texto e imágenes por ti.", "Describe your photo... You can use **markdown** formatting!": "Describe tu foto... ¡Puedes utilizar el formato **markdown**!", "Description": "Descripción", "Description (Optional)": "Descripción (opcional)", + "design": "diseño", "DESIGN": "DISEÑO", + "Destination Group": "Grupo Destino", + "Developer": "Desarrollador", "Device Hostname": "Nombre de host del dispositivo", "Disable All": "Desactivar todo", "Discard & Exit": "Descartar y salir", "Discard changes?": "¿Descartar cambios?", "Disconnect": "Desconecte", + "Discord": "Discordia", + "Discover": "Descubra", + "Discover Area": "Descubrir la zona", + "Discover new places in the current viewport": "Descubrir nuevos lugares en la ventana actual", "Display a customizable grid of selected photos": "Mostrar una cuadrícula personalizable de las fotos seleccionadas", "Display a filtered feed for a specific category with heading and content type filter": "Mostrar un feed filtrado para una categoría específica con filtro de encabezamiento y tipo de contenido.", "Display a single page card with details": "Mostrar una tarjeta de una sola página con detalles", @@ -197,6 +361,10 @@ "Display Name": "Mostrar nombre", "Display photos in a responsive grid layout": "Mostrar fotos en una cuadrícula adaptable", "Display the main home feed with photos, categories, and sorting": "Mostrar el feed de inicio principal con fotos, categorías y clasificación", + "Display the picture/post description beneath the title": "Mostrar la imagen/descripción del post debajo del título", + "Display the picture/post title": "Mostrar el título de la imagen/post", + "Display the selected category name as heading (when no custom heading is set)": "Mostrar el nombre de la categoría seleccionada como encabezamiento (cuando no se ha establecido un encabezamiento personalizado)", + "Does pm-image work on Windows?": "¿Funciona pm-image en Windows?", "Don't have an account? Sign up": "¿No tiene una cuenta? Regístrese", "Done": "Hecho", "Download": "Descargar", @@ -204,56 +372,95 @@ "Download English Translations": "Descargar traducciones al inglés", "Download JSON for {name}": "Descargar JSON para {nombre}", "Download Plot": "Descargar parcela", + "Download settings JSON": "Descargar JSON de configuración", + "Download Waypoints (GeoJSON)": "Descargar Waypoints (GeoJSON)", + "draft": "borrador", "Drag and resize widgets": "Arrastrar y cambiar el tamaño de los widgets", + "Drop <": "Drop <", + "Drop >": "Drop >", + "Dual": "Doble", + "Dual pane": "Doble cristal", "Dump JSON": "Volcar JSON", + "Duplicate": "Duplicar", "Duplicate Profile": "Duplicar perfil", "Duration (hh:mm:ss)": "Duración (hh:mm:ss)", "Duration:": "Duración:", "E": "E", "e.g. Cyberpunk Portrait": "por ejemplo, Cyberpunk Portrait", + "e.g. Home, Office": "Por ejemplo, Hogar, Oficina", + "e.g. Main Business, EU Branch": "Por ejemplo, empresa principal, sucursal de la UE", + "e.g. restaurant, lodging": "por ejemplo, restaurante, alojamiento", + "e.g. Ring doorbell, leave at reception…": "Por ejemplo, llamar al timbre, dejar en recepción...", + "e.g., HVAC contractors": "por ejemplo, contratistas de HVAC", "E.g., Quick Ramp Up": "Por ejemplo, Quick Ramp Up", "e.g., Start Heating": "p. ej., Iniciar calefacción", "e.g., Turn on coil for pre-heating stage": "p. ej., encender la bobina para la fase de precalentamiento", "Edit": "Editar", + "Edit Campaign": "Editar campaña", "Edit Details": "Editar detalles", "Edit Home Page": "Editar página de inicio", "Edit mode: Add, move, and configure widgets": "Modo edición: Añadir, mover y configurar widgets", "Edit mode: Configure containers and add widgets": "Modo edición: Configurar contenedores y añadir widgets", "Edit Picture": "Editar imagen", + "Edit Product": "Editar producto", "Edit Profile": "Editar perfil", "Edit Profile Page": "Editar página de perfil", "Edit Tags": "Editar etiquetas", "Edit Translations": "Editar traducciones", "Edit with AI Wizard": "Editar con el Asistente AI", + "email": "correo electrónico", "Email": "Correo electrónico", + "Email page": "Página de correo electrónico", + "Email sent successfully!": "Correo electrónico enviado correctamente", + "emails": "correos electrónicos", "EMails": "EMails", "Empty Canvas": "Lienzo vacío", + "Empty directory": "Directorio vacío", "Empty Layout": "Disposición vacía", "Empty Photo Card": "Tarjeta fotográfica vacía", "Enable All": "Activar todo", + "Enable Auto-Enrichments (Website & Email Lookup)": "Activar el enriquecimiento automático (búsqueda de sitios web y correo electrónico)", "Enable control unavailable for {name}": "Habilitar control no disponible para {nombre}", "Enabled": "Activado", "End Index": "Índice final", + "Enrich Visible Locations": "Enriquecer los lugares visibles", + "Enrichment": "Enriquecimiento", "Enter a title...": "Introduzca un título...", "Enter CP description": "Introduzca la descripción del CP", "Enter CP name": "Introduzca el nombre del PC", "Enter description...": "Introduzca la descripción...", "Enter display name": "Introduzca el nombre para mostrar", + "Enter group name...": "Introduzca el nombre del grupo...", "Enter heading...": "Introduzca el encabezamiento...", "Enter username": "Introducir nombre de usuario", + "Enter your AIMLAPI API Key": "Introduzca su clave API AIMLAPI", + "Enter your Bria API Key": "Introduzca su clave API de Bria", "Enter your email": "Introduzca su dirección de correo electrónico", "Enter your Google API key": "Introduce tu clave API de Google", + "Enter your Google API Key": "Introduzca su clave API de Google", + "Enter your HuggingFace API Key": "Introduzca su clave API de HuggingFace", "Enter your OpenAI API key": "Introduzca su clave API de OpenAI", + "Enter your OpenAI API Key": "Introduzca su clave API de OpenAI", + "Enter your OpenRouter API Key": "Introduzca su clave API de OpenRouter", "Enter your password": "Introduzca su contraseña", + "Enter your Replicate API Key": "Introduzca su clave de API de réplica", + "Enter your Scrapeless API Key": "Introduzca su clave API de Scrapeless", + "Enter your SerpAPI Key": "Introduzca su clave SerpAPI", + "Enter your SerpAPI search query for Google Maps.": "Introduce tu consulta de búsqueda SerpAPI para Google Maps.", "err": "err", + "Error": "Error", "ERROR": "ERROR", + "EUR": "EUR", "Ex": "Ex", + "Exclude Types (comma separated)": "Tipos de exclusión (separados por comas)", "Exit Fullscreen": "Salir de pantalla completa", + "Expand": "Ampliar", "Export": "Exportar", "Export JSON": "Exportar JSON", "Export to CSV": "Exportar a CSV", "Ext": "Ext", "Extr": "Extr", + "Extracted Web Content": "Contenido web extraído", "Extru": "Extru", "Extrud": "Extrud", "Extrude": "Extrusión", @@ -263,143 +470,285 @@ "Extrudrs": "Extrudrs", "Extrusion, compounding & recycling systems for education, labs, and small-scale production — backed by our knowledgebase, custom software, and manufacturing services.": "Sistemas de extrusión, composición y reciclaje para educación, laboratorios y producción a pequeña escala, respaldados por nuestra base de conocimientos, software personalizado y servicios de fabricación.", "failed": "fallido", + "Failed to flush cache": "Error al vaciar la caché", + "Failed to load content": "Error al cargar el contenido", + "Failed to load profile data": "Error al cargar los datos del perfil", + "Failed to load user profile": "Error al cargar el perfil de usuario", + "Failed to load your images": "Error al cargar las imágenes", + "Failed to save file": "Error al guardar el archivo", "Fallback image shown while video loads": "Se muestra una imagen de reserva mientras se carga el vídeo", + "FAQ (questions and answers)": "FAQ (preguntas y respuestas)", "Favorite Coils": "Bobinas favoritas", "Favorite Registers": "Registros favoritos", "Favorites": "Favoritos", + "Features at a glance": "Características", "Fields": "Campos", "File": "Archivo", "File Browser": "Navegador de archivos", "File name": "Nombre del fichero", + "File too large to preview": "Archivo demasiado grande para previsualizar", "File Tools": "Herramientas de archivo", "Files": "Archivos", + "FILES": "ARCHIVOS", "Fill": "Rellene", "Filling": "Relleno", "Filter": "Filtro", + "Filter by content type": "Filtrar por tipo de contenido", "Filter feed to show only content from a specific user": "Filtrar el feed para mostrar sólo el contenido de un usuario específico", + "Filter Files (Shift + +)": "Filtrar archivos (Mayús + +)", "Filter to show only a specific content type": "Filtro para mostrar sólo un tipo de contenido específico", "Finish": "Acabado", "Flex Container": "Contenedor flexible", + "Floating support chat button that expands into a full chat panel.": "Botón flotante de chat de soporte que se expande en un panel de chat completo.", + "Flush Cache": "Vaciar caché", + "Flush System Cache": "Vaciar la caché del sistema", + "Folders": "Carpetas", "followers": "seguidores", "following": "siguiente", + "For AI image generation": "Para generar imágenes de IA", "For AI image generation (stored securely)": "Para la generación de imágenes de IA (almacenadas de forma segura)", + "For AIMLAPI services": "Para los servicios AIMLAPI", + "For Bria AI services": "Para los servicios de Bria AI", + "For Google services": "Para los servicios de Google", "For Google services (stored securely)": "Para los servicios de Google (almacenados de forma segura)", + "For HuggingFace models": "Para los modelos HuggingFace", + "For OpenRouter AI models": "Para los modelos OpenRouter AI", + "For Replicate AI models": "Para replicar modelos de IA", + "For search engine results": "Para los resultados de los motores de búsqueda", + "For web scraping and crawling": "Para raspado y rastreo web", "Forgot password?": "¿Ha olvidado su contraseña?", + "Found": "Encontrado", + "Found places with these types will be ignored globally.": "Los lugares encontrados con estos tipos serán ignorados globalmente.", + "Found Types:": "Tipos encontrados:", + "Full Overlap": "Solapamiento total", + "Full Text Search (extract text from files)": "Búsqueda de texto completo (extraer texto de archivos)", "Full-width hero banner with background video, text overlay, and call-to-action buttons": "Banner principal de ancho completo con vídeo de fondo, texto superpuesto y botones de llamada a la acción", "Fullscreen": "Pantalla completa", "Fullscreen Editor": "Editor de pantalla completa", "fwd": "fwd", + "GADM Areas": "Áreas GADM", "Gallery": "Galería", "General": "General", "General Settings": "Configuración general", + "Generate AI Image": "Generar imagen AI", + "Generate AI images or upload your own photos to share.": "Genera imágenes AI o sube tus propias fotos para compartirlas.", + "Generate an image from a text prompt": "Generar una imagen a partir de un texto", + "Generate complete pages with AI-powered text and images.": "Genere páginas completas con texto e imágenes basados en IA.", + "Generate Text Only": "Generar sólo texto", "Generate Title & Description with AI": "Generar título y descripción con IA", + "Generate with AI": "Generar con IA", + "Generate with Images": "Generar con imágenes", + "Generating content...": "Generar contenidos...", + "Generating page content...": "Generar contenido de página...", + "Getting started checklist": "Lista de control para empezar", + "GHS Centers": "Centros GHS", + "GHS Data Thresholds": "Umbrales de datos del SGA", + "Global Groups": "Grupos mundiales", "Global Settings": "Ajustes globales", "Glossaries": "Glosarios", + "Go to app": "Ir a la aplicación", "Google API Key": "Clave API de Google", + "Gracefully restart the server process. Systemd will automatically bring it back online. This will cause a brief downtime.": "Reinicie el proceso del servidor. Systemd lo volverá a poner en línea automáticamente. Esto causará un breve tiempo de inactividad.", + "Grant": "Subvención", + "Grant Access": "Conceder acceso", + "Grid Columns": "Columnas de cuadrícula", + "Grid Generation Limit": "Límite de generación de la red", + "Grid search": "Búsqueda en la cuadrícula", + "Grid Search": "Búsqueda en la cuadrícula", + "Grid Simulation Mode": "Modo de simulación de red", + "Grid:": "Rejilla:", + "GridSearch": "GridSearch", + "Group": "Grupo", + "group selected": "grupo seleccionado", + "Groups": "Grupos", "Hardware I/O": "E/S de hardware", "Heading": "Rúbrica", "Heading Level": "Rúbrica Nivel", "Heating Time": "Tiempo de calentamiento", "Help": "Ayuda", + "Help and version": "Ayuda y versión", + "Hero": "Héroe", "HEX": "HEX", + "Hex Geometric Grid": "Cuadrícula geométrica hexagonal", "Hidden": "Oculto", "HIDDEN": "OCULTA", "Hierarchy": "Jerarquía", "History": "Historia", "HMI Edit Mode Active": "Modo Edición HMI Activo", "Home": "Inicio", + "HOME": "INICIO", "Home Feed": "Inicio", + "Homepage": "Página de inicio", + "Homepage Preview": "Vista previa de la página de inicio", "HomingAuto": "HomingAuto", "HomingMan": "HomingMan", "Hostname": "Nombre de host", "How dark the text background pane is": "Cómo de oscuro es el panel de fondo del texto", + "How do I batch resize images in a folder?": "¿Cómo cambio por lotes el tamaño de las imágenes de una carpeta?", + "How do I resize an image to an exact width?": "¿Cómo redimensiono una imagen a una anchura exacta?", + "How do I resize without losing quality?": "¿Cómo puedo cambiar el tamaño sin perder calidad?", + "How does caching work?": "¿Cómo funciona la caché?", + "How It Works: The TypeScript Engine": "Cómo funciona: El motor TypeScript", + "How to resize images (quick start)": "Cómo cambiar el tamaño de las imágenes (inicio rápido)", "HTML Content": "Contenido HTML", "HTML heading level": "Nivel de encabezamiento HTML", + "https://…": "https://...", + "HuggingFace API Key": "Clave API de HuggingFace", "I": "I", "I18n": "I18n", "I18N": "I18N", "ID of the category to filter by (use the picker in edit mode)": "ID de la categoría por la que filtrar (utilice el selector en modo edición)", "ID:": "ID:", + "Identity & Access": "Identidad y acceso", "Idle": "Ocioso", "IDLE": "IDLE", + "If a region is physically missing GHS raster data, allow it to pass instead of auto-failing": "Si a una región le faltan físicamente datos ráster GHS, permita que pase en lugar de que falle automáticamente.", + "If enabled, it ignores ALL density, elevation, geometry overlap, and custom filters. Will push all raw items": "Si se activa, ignora TODOS los filtros de densidad, elevación, superposición de geometría y personalizados. Empujará todos los elementos sin procesar", + "Image": "Imagen", "Image Tools": "Herramientas de imagen", + "Image Tools enabled: AI can generate and embed images in the content": "Herramientas de imagen habilitadas: La IA puede generar e incrustar imágenes en el contenido", "Image URL...": "URL de la imagen...", + "Images": "Imágenes", + "IMAP Integrations": "Integraciones IMAP", "Import": "Importar", + "Import Complete": "Importación completa", + "Import Contacts": "Importar contactos", "Import JSON": "Importar JSON", + "Import settings JSON": "Importar configuración JSON", + "Imported {{count}} contacts successfully": "Importados {{count}} contactos con éxito", + "Impressum": "Impresionante", "In": "En", "in seconds": "en segundos", "Include page context": "Incluir el contexto de la página", + "Include page title": "Incluir el título de la página", + "Index Mount": "Monte índice", "info": "información", "Info": "Información", + "Initial Prompt...": "Pregunta inicial...", + "Initial Search Query": "Consulta de búsqueda inicial", "Injectors": "Inyectores", "Insert Link": "Insertar enlace", + "Inspected Location": "Ubicación inspeccionada", "Integrations": "Integraciones", "Interactive gallery with main viewer and filmstrip navigation": "Galería interactiva con visor principal y navegación por películas", + "Interactive map with clustering, regional analysis, and grid search simulator.": "Mapa interactivo con agrupación, análisis regional y simulador de búsqueda de cuadrículas.", "Interlocked": "Entrelazados", + "Internal notes, instructions…": "Notas internas, instrucciones...", + "Interval between sends (seconds)": "Intervalo entre envíos (segundos)", "Invisible": "Invisible", + "ipc — one JSON line per connection (TCP; Unix socket on Linux/macOS)": "ipc - una línea JSON por conexión (TCP; socket Unix en Linux/macOS)", + "Is pm-image the same as Sharp?": "¿Es pm-image lo mismo que Sharp?", + "Is the product enabled?": "¿Está habilitado el producto?", + "item": "artículo", "items": "artículos", "Jammed": "Atascado", + "Jane Doe": "Jane Doe", + "jane@example.com": "jane@example.com", + "Job ID:": "ID del puesto:", "Joined": "Se unió a", "Joystick": "Joystick", + "JSON": "JSON", + "JSON map of type id → payload; edited in Product Edit or here.": "Mapa JSON de tipo id → carga útil; editado en Editar producto o aquí.", "Katbot - Injection Machine": "Katbot - Máquina de inyección", + "kb software": "software kb", "Keep Editing": "Seguir editando", + "Key Name": "Nombre clave", + "knowlede base": "base de conocimientos", "Knowledge Base": "Base de conocimientos", + "Known Types in Database": "Tipos conocidos en la base de datos", + "Label": "Etiqueta", "Language": "Idioma", "Last updated": "Última actualización", + "Last Updated": "Última actualización", "Last updated:": "Última actualización:", "Latest": "Última", + "Latest Articles": "Últimos artículos", "Latest Articles ": "Latest Articles", + "Latest Posts": "Últimas entradas", "Latest Posts ": "Latest Posts", - "Layouts": "Layouts", + "Launch Search": "Iniciar búsqueda", + "Launching...": "Lanzamiento...", + "Layout": "Diseño", + "Layouts": "Diseños", "LAYOUTS": "LAYOUTS", "Level": "Nivel", + "Light engineering: sizing for 20 mm stainless rods": "Ingeniería ligera: dimensionamiento para barras de acero inoxidable de 20 mm", "like": "como", "likes": "le gusta", "Link title...": "Título del enlace...", + "Load feed matching this search query": "Cargar feed que coincida con esta consulta", "LOADCELL": "CELDA DE CARGA", "Loadcell[25]": "Célula de carga[25]", "Loadcell[26]": "Célula de carga[26]", + "Loading 3D Engine...": "Cargando Motor 3D...", + "Loading addresses…": "Cargando direcciones...", + "Loading analytics...": "Cargando análisis...", "Loading Cassandra settings...": "Cargando configuración de Cassandra...", "Loading comments...": "Cargando comentarios...", "Loading Editor...": "Cargando Editor...", "Loading gallery items...": "Cargando elementos de la galería...", "Loading gallery...": "Cargando galería...", + "Loading groups...": "Cargando grupos...", "Loading network settings...": "Cargando configuración de red...", "Loading page...": "Cargando página...", "Loading picture...": "Cargando imagen...", + "Loading pictures...": "Cargando imágenes...", + "Loading products...": "Cargando productos...", "Loading profile...": "Cargando perfil...", "Loading profiles from Modbus...": "Cargando perfiles de Modbus...", + "Loading search results...": "Cargando resultados de búsqueda...", + "Loading system metrics...": "Cargando métricas del sistema...", + "Loading vendor profiles…": "Cargando perfiles de proveedores...", "Loading versions...": "Cargando versiones...", "Loading...": "Cargando...", "Loading…": "Loading…", + "Locked": "Bloqueado", + "Logic when filters are active.": "Lógica cuando los filtros están activos.", "Logs": "Registros", "Logs Dashboard": "Cuadro de mandos de registros", "Low": "Bajo", "Lydia - Plastic Extruder - Robot Printhead - SJ25": "Lydia - Extrusora de plástico - Cabezal de impresión robotizado - SJ25", + "machines": "máquinas", "Machines": "Máquinas", "Main": "Principal", "Main banner heading text": "Texto del encabezamiento del banner principal", "Make this picture visible to others": "Haz que esta foto sea visible para los demás", "Manage": "Gestione", + "Manage and preview your type definitions": "Gestione y previsualice sus definiciones de tipos", "Manage Categories": "Gestionar categorías", "Manage categories and organize your content structure.": "Gestione categorías y organice su estructura de contenidos.", + "Manage permissions for": "Gestionar permisos para", + "Manage saved snapshots of this page.": "Gestionar las instantáneas guardadas de esta página.", "Manage slave devices (max 1).": "Gestionar dispositivos esclavos (máx. 1).", + "Manage users, global resource groups, and hierarchical permissions.": "Gestione usuarios, grupos de recursos globales y permisos jerárquicos.", "Manage your account settings and preferences": "Gestionar la configuración y las preferencias de su cuenta", "MANUAL": "MANUAL", "MANUAL MULTI": "MANUAL MULTI", "MANUAL_MULTI": "MANUAL_MULTI", + "Map capped at": "Mapa limitado a", + "March Newsletter": "Boletín de marzo", "Markdown": "Markdown", + "Mask / Filter": "Máscara / Filtro", "Master Configuration": "Configuración maestra", "Master Name": "Nombre principal", + "Materials and environment": "Materiales y medio ambiente", "Max": "Max", + "Max Elevation (m)": "Elevación máxima (m)", "Max Simultaneous": "Máximo simultáneo", "MAX_TIME": "TIEMPO_MAX", + "Maximum number of results to fetch per grid cell from Google Maps. Allowed range: 1-20. Lower limits save credits and time.": "Número máximo de resultados que se pueden obtener de Google Maps por celda de la cuadrícula. Rango permitido: 1-20. Los límites más bajos ahorran créditos y tiempo.", "MAXLOAD": "MAXLOAD", "Media": "Medios de comunicación", + "Members of these groups will inherently be granted access to this product.": "Los miembros de estos grupos tendrán acceso intrínseco a este producto.", + "Menu": "Menú", "messages in context": "mensajes en contexto", + "meta": "meta", "Meta": "Meta", "Mid": "Medio", "Min": "Min", + "Min Density (p/km²)": "Densidad mínima (p/km²)", + "Min GHS Built Area": "Superficie mínima construida SGA", + "Min GHS Population": "Min GHS Población", "Minimum Height (px)": "Altura mínima (px)", "Minimum height of the banner in pixels": "Altura mínima del banner en píxeles", "MINLOAD": "CARGA MÍNIMA", @@ -407,38 +756,71 @@ "Mode": "Modo", "modified": "modificado", "Moulds": "Moldes", + "Mount": "Monte", "Move control point down": "Mover el punto de control hacia abajo", "Move control point up": "Mover el punto de control hacia arriba", "MULTI_TIMEOUT": "MULTI_TIMEOUT", + "My Files": "Mis archivos", "My only": "Mi único", "My Profile": "Mi perfil", + "My Purchases": "Mis compras", "N/A": "N/A", + "Name": "Nombre", + "Native GADM Regions": "Regiones GADM nativas", + "Navigation menu with custom, page, and category links": "Menú de navegación con enlaces personalizados, de página y de categoría", "Nested Layout Container": "Contenedor de diseño anidado", "Network": "Red", "Network Settings": "Ajustes de red", "New": "Nuevo", + "New Campaign": "Nueva campaña", "New Chat": "Nuevo chat", + "New Group Name": "Nombre del nuevo grupo", + "New group name…": "Nuevo nombre de grupo...", + "New Page": "Página nueva", + "New Tab": "Nueva pestaña", "New term...": "Nuevo plazo...", + "New York": "Nueva York", + "Newsletter — March 2026": "Boletín de noticias - marzo de 2026", + "No active permissions found.": "No se han encontrado permisos activos.", "No categories found": "No se han encontrado categorías", "No coils data available. Try refreshing.": "No hay datos de bobinas disponibles. Prueba a actualizar.", "No comments yet": "Aún no hay comentarios", "No containers yet": "Aún no hay contenedores", "No enabled profile": "Perfil no habilitado", + "No file browser context": "Sin contexto de explorador de archivos", "No glossaries found.": "No se han encontrado glosarios.", + "No groups": "No hay grupos", + "No image available to save": "No hay imagen disponible para guardar", "No images selected": "No hay imágenes seleccionadas", + "No Level": "Sin nivel", "No Operation": "Ninguna operación", "No other versions available for this image.": "No hay otras versiones disponibles para esta imagen.", + "No Overlap (Dist)": "Sin solapamiento (Dist)", + "No page selected": "Ninguna página seleccionada", + "No picture selected": "No hay imagen seleccionada", "No pictures available": "No hay fotos disponibles", + "No pictures yet": "Aún no hay fotos", + "No post selected": "Ningún puesto seleccionado", + "No posts available": "No hay puestos disponibles", + "No regions picked yet.": "Aún no se han elegido regiones.", "No register data available. Try refreshing.": "No hay datos de registro disponibles. Prueba a actualizar.", "No saved sessions": "No hay sesiones guardadas", "No source found.": "No se ha encontrado ninguna fuente.", "No templates saved yet": "Aún no hay plantillas guardadas", + "No Type Selected": "No Tipo seleccionado", "No widget selected": "No se ha seleccionado ningún widget", "No widgets found": "No se han encontrado widgets", "No widgets yet": "Aún no hay widgets", "none": "ninguno", "None": "Ninguno", "NONE": "NONE", + "None (Don't Group)": "Ninguno (No agrupar)", + "Not available": "No disponible", + "Nota": "Nota", + "Note: Image Tools require an OpenAI provider and will use your selected OpenAI model": "Nota: Las herramientas de imagen requieren un proveedor de OpenAI y utilizarán el modelo de OpenAI seleccionado.", + "Notes / Source": "Notas / Fuente", + "Number": "Número", + "Number of columns to display in grid view": "Número de columnas a mostrar en la vista en cuadrícula", "OC": "OC", "OFFLINE": "FUERA DE LÍNEA", "Offset": "Desplazamiento", @@ -447,46 +829,71 @@ "ON": "EN", "ONLINE": "EN LÍNEA", "Open": "Abrir", + "Open details in new tab": "Abrir detalles en una nueva pestaña", "Open in full page": "Abrir en página completa", + "Open in new tab": "Abrir en una nueva pestaña", + "Open with AI": "Abierto con IA", "OpenAI API Key": "Clave API de OpenAI", + "OpenRouter API Key": "Clave API de OpenRouter", + "Operating Hours": "Horario de atención al público", "Operatorswitch": "Interruptor de operador", "Optimize": "Optimice", "Optimize prompt with AI": "Optimizar la rapidez con IA", "optional": "opcional", "Or continue with": "O continuar con", + "or drag and drop an image here": "o arrastre y suelte una imagen aquí", "Organizations": "Organizaciones", "Organize content into switchable tabs": "Organiza los contenidos en pestañas intercambiables", "OV": "OV", + "Overall progress": "Progreso general", "Overlay Darkness": "Oscuridad superpuesta", "OVERLOAD": "SOBRECARGA", + "Overview": "Visión general", + "Own": "Propio", "Page": "Página", "PAGE": "PÁGINA", "Page Card": "Tarjeta de página", + "Page created! Redirecting you now...": "¡Página creada! Redirigiéndote ahora...", "Page hidden": "Página oculta", "Page linked to this category": "Página vinculada a esta categoría", + "Page made public": "Página hecha pública", "Page saved": "Página guardada", "Page Title": "Título de la página", "Page Tools": "Herramientas de página", "Pages": "Páginas", "Parent": "Padres", + "Particular (sin intermediarios)": "Particular (sin intermediarios)", "Partitions": "Particiones", "Password": "Contraseña", "Paste": "Pegar", "Pasted 1 container(s)": "Pegado 1 contenedor(es)", "Pasted 1 widget(s)": "Pegado 1 widget(s)", + "Path": "Ruta", + "Path:": "Senda:", + "Pattern library: joints and how to use them": "Biblioteca de patrones: articulaciones y cómo utilizarlas", "Pause": "Pausa", "Pause or play background video": "Pausa o reproduce el vídeo de fondo", "Pause Profile": "Pausa Perfil", "Pause/Play video": "Pausa/Reproducir vídeo", "Permissions": "Permisos", + "Permissions & Tasks": "Permisos y tareas", "Phapp": "Phapp", + "Phone": "Teléfono", "Photo Card": "Tarjeta fotográfica", "Photo Grid": "Rejilla fotográfica", "Photo Grid Widget": "Widget de cuadrícula de fotos", + "Photos": "Fotos", + "Pick a background video or image": "Elige un vídeo o una imagen de fondo", + "Pick Image": "Elegir imagen", + "Picked Regions": "Regiones elegidas", + "pictures": "imágenes", "Pictures": "Fotos", "PID Control": "Control PID", + "Places": "Lugares", + "Plain": "Llano", "Play from start": "Jugar desde el principio", "Playground": "Parque infantil", + "Please select a region using the Area Selector first to define the search bounds.": "Seleccione primero una región con el selector de área para definir los límites de la búsqueda.", "Plunge": "Sumérgete", "Plunger": "Émbolo", "PlungingAuto": "PlungingAuto", @@ -495,6 +902,7 @@ "Polymech Controller": "Controlador Polymech", "Polymer Processing Equipment & Engineering Solutions": "Equipos de procesamiento de polímeros y soluciones de ingeniería", "Pop-out": "Desplegable", + "Pop:": "Pop:", "Post Comment": "Publicar comentario", "Poster Image": "Imagen del cartel", "PostFlow": "PostFlow", @@ -506,10 +914,14 @@ "Press Cylinder Controls": "Controles del cilindro de prensado", "Presscylinder": "Cilindro a presión", "Preview": "Vista previa", + "Preview & Simulate": "Previsualizar y simular", + "Print Debug Info to Console": "Imprimir información de depuración en la consola", "Priv": "Priv", "Private": "Privado", + "Product Settings": "Ajustes del producto", "Products": "Productos", "Products & Services": "Productos y servicios", + "Products Management": "Gestión de productos", "Profile": "Perfil", "Profile Curves": "Curvas de perfil", "Profile Name": "Nombre del perfil", @@ -523,17 +935,30 @@ "Properties": "Propiedades", "Properties:": "Propiedades:", "Provider & Model": "Proveedor y modelo", + "Provider ID": "ID de proveedor", "Pub": "Pub", "Public": "Público", + "Purchases": "Compras", "PV": "FV", + "Query": "Consulta", + "Quick build recipes": "Recetas de construcción rápida", + "Rating": "Clasificación", + "Raw Data": "Datos brutos", + "Ready to Launch": "Listo para el lanzamiento", "Real time Charting": "Gráficos en tiempo real", "Real-time Charts": "Gráficos en tiempo real", "Recent": "Recientes", + "Recipient Email": "Correo electrónico del destinatario", "Record": "Registro", "Record audio": "Grabar audio", + "Redirecting…": "Redirigiendo...", "Redo": "Rehacer", + "Reference Images": "Imágenes de referencia", + "References, example projects, and off‑the‑shelf parts (Amazon, McMaster‑Carr, and others)": "Referencias, proyectos de ejemplo y piezas estándar (Amazon, McMaster-Carr y otros)", "Refresh Rate": "Frecuencia de actualización", + "Regions": "Regiones", "Registers": "Registros", + "Related documentation": "Documentación relacionada", "REMOTE": "REMOTO", "Remove all": "Eliminar todo", "Remove all control points from this plot": "Eliminar todos los puntos de control de este gráfico", @@ -542,25 +967,40 @@ "Removed from category": "Eliminado de la categoría", "Render HTML content with variable substitution": "Renderizar contenido HTML con sustitución de variables", "Replay": "Reproducir", + "Replicate API Key": "Replicar clave API", + "Requests per window limit": "Límite de solicitudes por ventana", "reset": "reiniciar", "Reset": "Restablecer", "Reset Zoom": "Restablecer zoom", "reset_fault": "reset_fault", "ResettingJam": "ReiniciarJam", + "resize — cover (crop), contain (letterbox), rotate / flip": "redimensionar - cubrir (recortar), contener (letterbox), rotar / voltear", + "resize — fit inside a box (default), write WebP / AVIF by extension": "redimensionar - encajar dentro de una caja (por defecto), escribir WebP / AVIF por extensión", + "resize — square images (1:1)": "redimensionar - imágenes cuadradas (1:1)", + "Resolution (km)": "Resolución (km)", + "Resolving location...": "Resolver la ubicación...", "Resources": "Recursos", + "Restart": "Reinicie", "Restart at end": "Reinicio al final", + "Restart Server": "Reiniciar el servidor", + "Restore": "Restaurar", + "results": "resultados", "rev": "rev", + "reviews": "reseñas", + "revs": "revoluciones", "Root Category": "Categoría Raíz", "run": "ejecute", "Run Action": "Ejecutar acción", "Run this control point action now": "Ejecute ahora esta acción de punto de control", "S": "S", + "sales@acme.com": "sales@acme.com", "Samplesignalplot 0": "Gráfico de señal de muestreo 0", "Save": "Guardar", "Save All Settings": "Guardar todos los ajustes", "Save AP Settings": "Guardar configuración AP", "Save API Keys": "Guardar claves API", "Save As": "Guardar como", + "Save as file": "Guardar como archivo", "Save as Widget": "Guardar como widget", "Save Changes": "Guardar cambios", "Save current as template": "Guardar actual como plantilla", @@ -570,54 +1010,102 @@ "Save to DB": "Guardar en BD", "Save to DeepL": "Guardar en DeepL", "Saved": "Guardado", + "Saved to file browser": "Guardado en el explorador de archivos", "Saving...": "Ahorrar...", + "Saving…": "Ahorrar...", "Scale": "Escala", "Scale:": "Escala:", + "Scan Trajectory": "Trayectoria de exploración", + "Scanning...": "Escaneando...", + "scheduled": "programado", + "Scrapeless API Key": "Clave API Scrapeless", "Search": "Buscar en", + "Search and select regions above to build your list": "Busque y seleccione las regiones de arriba para crear su lista", + "Search articles, pictures, files ...": "Buscar artículos, imágenes, archivos ...", "Search by title or ID...": "Buscar por título o ID...", "Search categories...": "Buscar categorías...", + "Search Configuration & Metadata": "Configuración de búsqueda y metadatos", + "Search countries, regions, cities to add...": "Buscar países, regiones, ciudades para añadir...", + "Search Files (Ctrl+F / F3)": "Buscar archivos (Ctrl+F / F3)", + "Search groups…": "Buscar grupos...", + "Search Limit per grid cell (Max 100)": "Límite de búsqueda por celda de la cuadrícula (máx. 100)", "Search page or heading...": "Buscar página o título...", "Search pages...": "Buscar páginas...", "Search pictures, users, collections...": "Buscar fotos, usuarios, colecciones...", + "Search Position": "Buscar posición", + "Search posts...": "Buscar puestos...", + "Search Summary": "Resumen de la búsqueda", "Search Tools": "Herramientas de búsqueda", "Search...": "Buscar...", + "Search…": "Buscar...", + "secret": "secreto", + "Secret": "Secreto", "Section heading displayed above the feed": "Título de la sección que aparece sobre el feed", + "Section heading displayed above the feed (overrides category name)": "Título de la sección que aparece sobre el feed (sustituye al nombre de la categoría)", "Section heading...": "Título de la sección...", + "Secure sign-in via Google or company credentials": "Inicio de sesión seguro mediante credenciales de Google o de la empresa", + "Select": "Seleccione", "Select a category to filter the feed": "Seleccione una categoría para filtrar el feed", "Select a category to manage or link.": "Seleccione una categoría para gestionar o vincular.", "Select a category to see actions or click edit/add icons in the tree.": "Seleccione una categoría para ver las acciones o haga clic en los iconos de editar/añadir en el árbol.", "Select a control point to see its properties.": "Seleccione un punto de control para ver sus propiedades.", + "Select a destination group and start the import process.": "Seleccione un grupo de destino e inicie el proceso de importación.", "Select a destination plot. The content of \"{plotName}\" will overwrite the selected plot. This action cannot be undone.": "Seleccione una parcela de destino. El contenido de \"{plotName}\" sobrescribirá la parcela seleccionada. Esta acción no puede deshacerse.", "Select a destination profile. The content of \"{profileName}\" will overwrite the selected profile. This action cannot be undone.": "Seleccione un perfil de destino. El contenido de \"{nombredelperfil}\" sobrescribirá el perfil seleccionado. Esta acción no se puede deshacer.", + "Select a page…": "Seleccione una página...", "Select a plot to overwrite": "Seleccione una parcela para sobrescribir", "Select a profile to overwrite": "Seleccione un perfil para sobrescribir", "Select a register or coil address": "Seleccione una dirección de registro o de bobina", "Select a signal plot to associate and edit": "Seleccione un trazado de señal para asociar y editar", "Select a tab": "Seleccione una pestaña", + "Select a type from the sidebar to view its details, edit the schema, and preview the generated form.": "Seleccione un tipo en la barra lateral para ver sus detalles, editar el esquema y previsualizar el formulario generado.", + "Select a variable to configure": "Seleccione una variable para configurar", "Select a video-intern picture to use as background": "Seleccione una imagen de vídeo interna para utilizarla como fondo", "Select category...": "Seleccionar categoría...", + "Select File & Import": "Seleccione Archivo e Importar", "Select Known Coil...": "Seleccionar bobina conocida...", + "Select one or more countries, regions, or cities to perform the grid search.": "Seleccione uno o varios países, regiones o ciudades para realizar la búsqueda en la cuadrícula.", "Select Picture": "Seleccionar imagen", "Select source...": "Seleccionar fuente...", "Select type": "Seleccione el tipo", "Selected": "Selección", "Selected child profiles will start, stop, pause, and resume with this parent profile.": "Los perfiles hijos seleccionados se iniciarán, detendrán, pausarán y reanudarán con este perfil padre.", "Selected Images": "Imágenes seleccionadas", + "Selected Path": "Ruta seleccionada", "Selected Widget": "Widget seleccionado", + "Selection": "Selección", + "Send": "Enviar", + "Send a password reset link to your email address": "Enviar un enlace de restablecimiento de contraseña a su dirección de correo electrónico", + "Send campaign?": "¿Enviar campaña?", + "Send Email Preview": "Enviar vista previa de correo electrónico", + "Send From": "Enviar desde", "Send IFTTT Notification": "Enviar notificación IFTTT", + "sending": "enviando", + "Sending...": "Enviando...", + "Sending…": "Enviando...", + "sent": "enviado", + "Sent: 0/2, 2 failed": "Enviados: 0/2, 2 fallidos", + "Sent: 1/2, 1 failed": "Enviado: 1/2, 1 fallido", + "Sent: 2/2": "Enviado: 2/2", "Sequential Heating": "Calentamiento secuencial", "Sequential Heating Control": "Control de calefacción secuencial", "Series": "Serie", "Series settings": "Ajustes de la serie", "Series Toggles": "Interruptores de serie", + "SerpAPI Key": "Clave SerpAPI", + "serve — HTTP REST": "servir - HTTP REST", + "Server Management": "Gestión de servidores", "Sessions": "Sesiones", "Set All": "Fijar todo", "Set All SP": "Fijar todo SP", "Set as Default": "Fijar por defecto", + "Set group…": "Establecer grupo...", + "Set status…": "Establecer estado...", "Settings": "Ajustes", "Settings...": "Ajustes...", "setup": "configuración", "Sh": "Sh", + "Share Link": "Compartir enlace", "She": "Ella", "Shee": "Shee", "Sheet": "Hoja", @@ -626,21 +1114,40 @@ "Sheetpre": "Sheetpre", "Sheetpres": "Sheetpres", "Sheetpress": "Prensa", + "Shipping Addresses": "Direcciones de envío", + "Shortest": "El más corto", "Shortplot 70s": "Trama corta 70s", + "Show Actions": "Mostrar acciones", + "Show Author": "Mostrar autor", + "Show author avatar and name on feed cards": "Mostrar el avatar y el nombre del autor en las tarjetas de alimentación", + "Show Categories": "Mostrar categorías", "Show Categories Sidebar": "Mostrar la barra lateral de categorías", + "Show Category Name": "Mostrar nombre de categoría", + "Show Date": "Fecha del espectáculo", + "Show Description": "Descripción del espectáculo", "Show Footer": "Mostrar pie de página", + "Show Global Footer": "Mostrar pie de página global", "Show grid/large/list view toggle buttons": "Mostrar botones de alternancia de vista de cuadrícula/grande/lista", + "Show Last Updated": "Mostrar última actualización", "Show Layout Toggles": "Mostrar interruptores de diseño", "Show Legend": "Mostrar leyenda", + "Show likes and comments on feed cards": "Mostrar \"me gusta\" y comentarios en las tarjetas de alimentación", + "Show Parent Path": "Mostrar ruta padre", "Show PV": "Mostrar PV", + "Show Social": "Show Social", "Show Sort Bar": "Mostrar barra de clasificación", "Show SP": "Mostrar SP", + "Show subcategories": "Mostrar subcategorías", + "Show Table of Contents": "Mostrar índice", "Show the category tree sidebar on desktop": "Mostrar la barra lateral del árbol de categorías en el escritorio", "Show the site footer below the feed": "Mostrar el pie de página del sitio debajo del feed", "Show the sort and category toggle bar": "Mostrar la barra de clasificación y categorías", + "Show Title": "Mostrar título", + "Showing my pages": "Mostrar mis páginas", "Shredders": "Trituradoras", "Sign in": "Iniciar sesión", "Sign In": "Iniciar sesión", + "Sign in to continue": "Iniciar sesión para continuar", "Sign in to your account": "Acceda a su cuenta", "Sign out": "Cerrar sesión", "Signal Control Point Details": "Detalles del punto de control de señales", @@ -649,20 +1156,35 @@ "Signalplot 922 Slot 2": "Signalplot 922 Ranura 2", "Signalplot 923 Slot 3": "Signalplot 923 Ranura 3", "Signals": "Señales", + "Simple": "Simple", + "Simulated Processed": "Simulado Procesado", + "simulator uses full": "El simulador utiliza", + "Single": "Único", + "Single pane": "Una sola hoja", + "Skipped Calls": "Llamadas omitidas", "Slave Mode": "Modo esclavo", "Slave:": "Esclavo:", "Slaves": "Esclavos", "Slot": "Ranura", "Slot:": "Ranura:", + "Slug": "Babosa", + "SMTP Servers": "Servidores SMTP", + "Snake": "Serpiente", + "Snapshot": "Instantánea", "Snippets": "Recortes", + "Social": "Social", "Software": "Software", "Sort By": "Ordenar por", + "Sort: name (asc)": "Ordenar: nombre (ascendente)", "Source": "Fuente", "SP": "SP", "SP CMD Addr:": "SP CMD Addr:", "SP:": "SP:", "specs": "especificaciones", "Specs Table": "Tabla de especificaciones", + "Spiral In": "En espiral", + "Spiral Out": "Salida en espiral", + "Square Geometric Grid": "Cuadrícula geométrica cuadrada", "STA Gateway": "Pasarela STA", "STA IP Address": "Dirección IP STA", "STA Password": "Contraseña STA", @@ -672,29 +1194,50 @@ "STA Subnet Mask": "Máscara de subred STA", "STALLED": "BLOQUEADO", "Start": "Inicio", + "Start Import": "Iniciar importación", "Start Index": "Inicio Índice", "Start PID Controllers": "Iniciar controladores PID", "Start Profile": "Iniciar perfil", + "Starting…": "Empezando...", "State:": "Estado:", "Station (STA) Mode": "Modo Estación (STA)", + "Stats": "Estadísticas", "stop": "stop", "Stop": "Stop", "Stop and reset": "Parar y reiniciar", "Stop at end": "Parada al final", "Stop PID Controllers": "Detener reguladores PID", "Stop Profile": "Detener Perfil", + "Stop search": "Detener la búsqueda", "Stopped": "Detenido", "Stopping": "Detener", + "Street View": "Vista de la calle", + "String": "Cadena", "Structure": "Estructura", + "Structure and alias types enabled for this product. You can also change them under Product Settings.": "Tipos de estructura y alias habilitados para este producto. También puede cambiarlos en Configuración del producto.", + "Structured fields from the types selected above (saved with this form).": "Campos estructurados de los tipos seleccionados anteriormente (guardados con este formulario).", + "Subject": "Asunto", + "Subs": "Suplentes", + "Support Chat": "Chat de asistencia", + "Support Chat (Instant Quotes)": "Chat de asistencia (presupuestos instantáneos)", + "Support Chat Widget": "Widget de chat de asistencia", + "Switch mount": "Montaje del interruptor", "Switch to edit mode to add containers": "Cambiar al modo de edición para añadir contenedores", "Switch to edit mode to add widgets": "Cambiar al modo de edición para añadir widgets", + "System": "Sistema", "System Calls": "Llamadas al sistema", + "System Control": "Control del sistema", + "System Default (config.json)": "Sistema por defecto (config.json)", "System Information": "Información del sistema", "System Messages": "Mensajes del sistema", "System Prompt": "Indicación del sistema", + "System Resources": "Recursos del sistema", "Table of Contents": "Índice", "Tabs Widget": "Widget de pestañas", + "Tags": "Etiquetas", + "Target Calls": "Llamadas selectivas", "Target Controllers (Registers)": "Controladores de destino (registros)", + "Target groups": "Grupos destinatarios", "Tell us about yourself...": "Háblenos de usted...", "Temperature Control Points": "Puntos de control de temperatura", "Temperature Profiles": "Perfiles de temperatura", @@ -704,17 +1247,34 @@ "terms": "términos", "terms to DeepL + DB": "términos a DeepL + DB", "Text Block": "Bloque de texto", + "The C++ Batch Generator Pipeline": "El proceso del generador de lotes de C", + "These types drive the \"Type data\" section when editing this product.": "Estos tipos dirigen la sección \"Datos de tipo\" al editar este producto.", + "This action cannot be undone and will disconnect any associated ACLs.": "Esta acción no se puede deshacer y desconectará cualquier ACL asociada.", "This hostname is used for both STA and AP modes. Changes here will be saved with either form.": "Este nombre de host se utiliza tanto para los modos STA como AP. Los cambios aquí se guardarán con cualquiera de los dos modos.", "This is where you'll design and configure your HMI layouts.": "Aquí es donde diseñará y configurará sus diseños de HMI.", "This will permanently clear the profile \"{profileName}\" from the server. This action cannot be undone.": "Esto borrará permanentemente el perfil \"{profileName}\" del servidor. Esta acción no se puede deshacer.", + "This will send the email to all contacts in the target groups. This cannot be undone.": "Esto enviará el correo electrónico a todos los contactos de los grupos objetivo. Esto no se puede deshacer.", + "Time window in seconds": "Ventana de tiempo en segundos", "Time:": "Hora:", "Timeline:": "Calendario:", + "Tiras para arrancar (opcional)": "Tiras para arrancar (opcional)", "Title (Optional)": "Título (opcional)", + "Toggle the display of the author name.": "Conmutar la visualización del nombre del autor.", + "Toggle the display of the category paths.": "Conmutar la visualización de las rutas de categorías.", + "Toggle the display of the global app footer site-wide.": "Activa la visualización del pie de página global de la aplicación en todo el sitio.", + "Toggle the display of the last updated footer.": "Conmutar la visualización del último pie de página actualizado.", + "Toggle the display of the page title block.": "Activar la visualización del bloque de título de la página.", + "Toggle the display of the parent page path above the title.": "Activa la visualización de la ruta de la página principal sobre el título.", + "Toggle the display of the publish date.": "Conmutar la visualización de la fecha de publicación.", + "Toggle the display of the side table of contents.": "Activa la visualización del índice lateral.", + "Toggle the display of the top-right page actions menu.": "Activar la visualización del menú de acciones de la parte superior derecha de la página.", "Tools": "Herramientas", "Top": "Top", + "total": "total", "Total": "Total", "Total Cost": "Coste total", "Total:": "Total:", + "Tracking ID": "ID de seguimiento", "translatable items": "elementos traducibles", "Translate All Missing": "Traducir todo lo que falta", "Translate via DeepL": "Traducir a través de DeepL", @@ -723,37 +1283,75 @@ "Translation...": "Traducción...", "translations": "traducciones", "translations to database": "traducciones a la base de datos", + "Type": "Tipo", + "Type data": "Tipo de datos", + "Type data (per type id)": "Datos de tipo (por id de tipo)", + "Type Properties": "Propiedades de tipo", + "Type updated": "Tipo actualizado", "Type:": "Tipo:", + "Types": "Tipos", + "Types Editor": "Tipos Editor", "Undo": "Deshacer", + "United States": "Estados Unidos", "Unknown": "Desconocido", "Unlisted": "Sin clasificar", + "unsubscribed": "darse de baja", "Update": "Actualización", "Update Profile": "Actualizar perfil", "Updated i18n files": "Archivos i18n actualizados", "Upload": "Cargar", "Upload All JSON": "Cargar todo el JSON", + "Upload as Picture": "Cargar como imagen", "Upload Images": "Cargar imágenes", "Upload images or select from gallery": "Sube imágenes o selecciónalas de la galería", "Upload JSON for {name}": "Subir JSON para {nombre}", "Upload Plot": "Cargar parcela", + "Upload Video": "Cargar vídeo", + "Upload videos to share with the community. Automatic processing and optimization.": "Sube vídeos para compartirlos con la comunidad. Procesamiento y optimización automáticos.", + "Use my current location": "Utilizar mi ubicación actual", "User Defined": "Definido por el usuario", "User Filter": "Filtro de usuario", "User not found": "Usuario no encontrado", + "User profile updated successfully": "Perfil de usuario actualizado correctamente", "Username": "Nombre de usuario", + "Value": "Valor", "Value:": "Valor:", "Variables": "Variables", + "Vendor Profiles": "Perfiles de proveedores", + "versions": "versiones", "Versions": "Versiones", "Video Banner": "Video Banner", "View": "Ver", "VIEW": "VER", + "View Dump": "Ver Vertedero", "View Mode": "Ver Modo", "View mode: Interact with your widgets": "Modo de visualización: Interactúa con tus widgets", + "View on Map": "Ver en el mapa", + "View Options": "Ver opciones", "View parent page": "View parent page", + "Visibility": "Visibilidad", "Visible": "Visible", "Visible Controllers": "Controladores visibles", + "Visit": "Visite", + "Visit Website": "Visitar el sitio web", + "Visual": "Visual", + "Voice + AI": "Voz + IA", "Watched Items": "Artículos vigilados", + "Web Search": "Búsqueda en la Web", + "Website": "Página web", + "Welcome": "Bienvenido", + "Welcome back": "Bienvenido de nuevo", "Welcome Back": "Bienvenido de nuevo", + "What are you looking for?": "¿Qué busca?", + "What is IPC for?": "¿Para qué sirve la CIP?", + "What is pm-image?": "¿Qué es pm-image?", + "What would you like to create?": "¿Qué le gustaría crear?", "When Slave Mode is enabled, all Omron controllers will be disabled for processing.": "Cuando se activa el modo esclavo, todos los controladores Omron se desactivarán para el procesamiento.", + "Where do you want to search?": "¿Dónde quiere buscar?", + "Where is the full API reference?": "¿Dónde está la referencia completa de la API?", + "Who is this for?": "¿A quién va dirigido?", + "Why this system works": "Por qué funciona este sistema", + "Why We Built This": "Por qué construimos esto", "Widget editor and drag-and-drop functionality coming soon...": "Próximamente, editor de widgets y función de arrastrar y soltar...", "Widget not found": "Widget no encontrado", "Widget translation deleted": "Traducción del widget eliminada", @@ -762,6 +1360,7 @@ "WIDGETS": "WIDGETS", "Window (min)": "Ventana (min)", "Window Offset": "Desplazamiento de la ventana", + "Wood Working Machines": "Máquinas para trabajar la madera", "Write Coil": "Bobina de escritura", "Write GPIO": "Escribir GPIO", "Write Holding Register": "Registro de retención de escritura", @@ -769,6 +1368,11 @@ "Y-Axis Left": "Eje Y Izquierda", "You are a helpful assistant...": "Eres un ayudante servicial...", "You have unsaved changes. Are you sure you want to discard them and exit?": "Tienes cambios sin guardar. ¿Estás seguro de que quieres descartarlos y salir?", + "You're all set. Let's get to work.": "Ya está todo listo. Manos a la obra.", "Your preferred language for the interface": "Su idioma preferido para la interfaz", - "your.email@example.com": "your.email@example.com" + "your.email@example.com": "your.email@example.com", + "Zero selected": "Cero seleccionado", + "Zig-Zag": "Zig-Zag", + "Zoom in": "Ampliar", + "Zoom out": "Alejar" } diff --git a/packages/ui/src/modules/places/PlacesGridView.tsx b/packages/ui/src/modules/places/PlacesGridView.tsx index 6e52e189..e5ec6050 100644 --- a/packages/ui/src/modules/places/PlacesGridView.tsx +++ b/packages/ui/src/modules/places/PlacesGridView.tsx @@ -1,6 +1,6 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { type GridPreset, getPresetVisibilityModel } from './useGridColumns'; +import { type GridPreset, getPresetVisibilityModel, GRID_TYPES_FILTER_EXCLUDES_ANY_OF } from './useGridColumns'; import { DataGrid, useGridApiRef, @@ -13,7 +13,8 @@ import { type GridFilterModel, type GridPaginationModel, type GridSortModel, - type GridColumnVisibilityModel + type GridColumnVisibilityModel, + GridLogicOperator, } from '@mui/x-data-grid'; import { type PlaceFull } from '@polymech/shared'; import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'; @@ -32,6 +33,22 @@ import { } from './gridUtils'; import { useGridColumns } from './useGridColumns'; +/** Matches email column `valueGetter` / `EmailCell` — used when seeding type excludes so the only MUI filter row can stay on `types` (Community grid allows one filter item). */ +function rowHasLeadEmail(row: any): boolean { + const emails = row.emails || row.meta?.emails || []; + if (Array.isArray(emails)) { + const s = emails.map((e: any) => (typeof e === 'string' ? e : e?.email)).filter(Boolean).join(', '); + return s.trim().length > 0; + } + if (typeof emails === 'string') return emails.trim().length > 0; + return false; +} + +export type CompetitorsGridViewHandle = { + /** Row IDs (place ids) that pass the current grid filter model and quick filter. */ + getFilteredPlaceIds: () => string[]; +}; + interface PlacesGridViewProps { competitors: PlaceFull[]; loading: boolean; @@ -42,6 +59,10 @@ interface PlacesGridViewProps { isOwner?: boolean; isPublic?: boolean; preset?: GridPreset; + /** When set, seeds `types` column filters (doesNotContain) so rows with these types can be removed from the filter panel without changing saved settings. */ + seedExcludeTypeFilters?: string[]; + /** When false, filter/sort/pagination changes are not written to the URL (avoids collisions with multiple `types` filters). */ + persistFiltersInUrl?: boolean; } const CustomToolbar = ({ selectedCount }: { selectedCount: number }) => { @@ -64,7 +85,7 @@ const CustomToolbar = ({ selectedCount }: { selectedCount: number }) => { ); }; -export const CompetitorsGridView: React.FC = ({ +export const CompetitorsGridView = forwardRef(function CompetitorsGridView({ competitors, loading, settings, @@ -73,25 +94,41 @@ export const CompetitorsGridView: React.FC = ({ onSelectPlace, isOwner = false, isPublic = false, - preset = 'full' -}) => { + preset = 'full', + seedExcludeTypeFilters, + persistFiltersInUrl = true, +}, ref) { const muiTheme = useMuiTheme(); const [searchParams, setSearchParams] = useSearchParams(); // Initialize state from URL params const [filterModel, setFilterModel] = useState(() => { - const fromUrl = paramsToFilterModel(searchParams); - if (fromUrl.items.length === 0 && !searchParams.has('nofilter')) { - // Only apply default "valid leads" filter if we are NOT in a public stripped view + // Community `DataGrid` forces a single filter item — encode all type excludes in one `types` filter (see `excludesAnyOf` in useGridColumns). Email "is not empty" is applied via `displayRows` when seeding type excludes. + if (seedExcludeTypeFilters?.length) { + const list = seedExcludeTypeFilters.map((t) => String(t).trim()).filter(Boolean); + return { + items: list.length + ? [ + { + field: 'types', + operator: GRID_TYPES_FILTER_EXCLUDES_ANY_OF, + value: JSON.stringify(list), + id: 'seed-excludes-types', + }, + ] + : [], + logicOperator: GridLogicOperator.And, + }; + } + const fromUrl = persistFiltersInUrl ? paramsToFilterModel(searchParams) : { items: [] as GridFilterModel['items'] }; + let items = [...(fromUrl.items || [])]; + if (items.length === 0 && !searchParams.has('nofilter')) { const shouldHideEmptyEmails = !isPublic || isOwner; - if (shouldHideEmptyEmails) { - return { - items: [{ field: 'email', operator: 'isNotEmpty' }] - }; + items.push({ field: 'email', operator: 'isNotEmpty', id: 'default-email' }); } } - return fromUrl; + return { items, logicOperator: GridLogicOperator.And }; }); const [paginationModel, setPaginationModel] = useState(() => { @@ -114,6 +151,14 @@ export const CompetitorsGridView: React.FC = ({ return true; }); }, [competitors]); + + const displayRows = React.useMemo(() => { + if (!seedExcludeTypeFilters?.length) return filteredCompetitors; + const shouldHideEmptyEmails = !isPublic || isOwner; + if (!shouldHideEmptyEmails) return filteredCompetitors; + return filteredCompetitors.filter(rowHasLeadEmail); + }, [filteredCompetitors, seedExcludeTypeFilters, isPublic, isOwner]); + // Column Widths state const [columnWidths, setColumnWidths] = useState>(() => { try { @@ -142,6 +187,18 @@ export const CompetitorsGridView: React.FC = ({ const [highlightedRowId, setHighlightedRowId] = useState(null); const [anchorRowId, setAnchorRowId] = useState(null); + useImperativeHandle(ref, () => ({ + getFilteredPlaceIds: () => { + const api = apiRef.current; + if (!api) return []; + const allSortedIds = api.getSortedRowIds?.() || []; + const filteredRowsLookup = (api as any).state?.filter?.filteredRowsLookup || {}; + return allSortedIds + .filter((id) => filteredRowsLookup[id] !== false) + .map((id) => String(id)); + }, + }), []); + // Sync local highlighted state with global selectedPlaceId useEffect(() => { if (selectedPlaceId !== highlightedRowId) { @@ -170,6 +227,10 @@ export const CompetitorsGridView: React.FC = ({ const handleFilterModelChange = (newFilterModel: GridFilterModel) => { setFilterModel(newFilterModel); + if (!persistFiltersInUrl) { + return; + } + setSearchParams(prev => { const newParams = new URLSearchParams(prev); @@ -425,7 +486,7 @@ export const CompetitorsGridView: React.FC = ({ row.place_id || row.placeId || row.id} loading={loading} @@ -548,4 +609,4 @@ export const CompetitorsGridView: React.FC = ({
); -}; +}); diff --git a/packages/ui/src/modules/places/PlacesMapView.tsx b/packages/ui/src/modules/places/PlacesMapView.tsx index d2f4c9e4..adf89f69 100644 --- a/packages/ui/src/modules/places/PlacesMapView.tsx +++ b/packages/ui/src/modules/places/PlacesMapView.tsx @@ -127,7 +127,7 @@ interface PlacesMapViewProps { export const PlacesMapView: React.FC = ({ places, onMapCenterUpdate, initialCenter, initialZoom, initialPitch, initialBearing, onMapMove, enrich, isEnriching, enrichmentProgress, initialGadmRegions, initialSimulatorSettings, simulatorSettings, onSimulatorSettingsChange, liveAreas = [], liveRadii = [], liveNodes = [], liveScanner, preset = 'SearchView', customFeatures, onRegionsChange, isPosterMode, onClosePosterMode, posterTheme: controlledPosterTheme, setPosterTheme: setControlledPosterTheme, selectedPlaceId, onSelectPlace, onMapReady }) => { const features: MapFeatures = useMemo(() => { if (isPosterMode) { - return { ...MAP_PRESETS['Minimal'], enableSidebarTools: false }; + return { ...MAP_PRESETS['Minimal'], enableSidebarTools: false, ...customFeatures }; } return { ...MAP_PRESETS[preset], diff --git a/packages/ui/src/modules/places/client-gridsearch.ts b/packages/ui/src/modules/places/client-gridsearch.ts index 7919773f..44c49b17 100644 --- a/packages/ui/src/modules/places/client-gridsearch.ts +++ b/packages/ui/src/modules/places/client-gridsearch.ts @@ -176,10 +176,18 @@ export const fetchPlacesGridSearchExport = async (jobId: string, format: 'md' | return apiClient(`/api/places/gridsearch/export?search=${jobId}&format=${format}`); }; -export const exportGridSearchToContacts = async (jobId: string, groupId?: string): Promise<{ imported: number; skipped: number }> => { +export const exportGridSearchToContacts = async ( + jobId: string, + groupId?: string, + opts?: { placeIds?: string[]; excludedTypes?: string[] }, +): Promise<{ imported: number; skipped: number }> => { return apiClient<{ imported: number; skipped: number }>(`/api/places/gridsearch/${jobId}/export-to-contacts`, { method: 'POST', - body: JSON.stringify({ groupId }) + body: JSON.stringify({ + groupId, + ...(opts?.placeIds !== undefined ? { placeIds: opts.placeIds } : {}), + ...(opts?.excludedTypes !== undefined ? { excludedTypes: opts.excludedTypes } : {}), + }), }); }; diff --git a/packages/ui/src/modules/places/components/map-layers/LocationLayers.tsx b/packages/ui/src/modules/places/components/map-layers/LocationLayers.tsx index 54bdef75..52a14fed 100644 --- a/packages/ui/src/modules/places/components/map-layers/LocationLayers.tsx +++ b/packages/ui/src/modules/places/components/map-layers/LocationLayers.tsx @@ -12,6 +12,19 @@ export interface LocationLayersProps { const emptyFc: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: [] }; +/** Keep cluster / point layers above GADM region overlays (picker may add layers later). */ +function bringLocationLayersToFront(m: maplibregl.Map) { + const order = ['clusters', 'cluster-count', 'unclustered-point-circle', 'unclustered-point-label']; + for (const id of order) { + if (!m.getLayer(id)) continue; + try { + m.moveLayer(id); + } catch { + /* style swap race */ + } + } +} + export function LocationLayers({ map, competitors, @@ -189,6 +202,8 @@ export function LocationLayers({ map.on('mouseleave', 'clusters', () => map.getCanvas().style.cursor = ''); map.on('mouseleave', 'unclustered-point-circle', () => map.getCanvas().style.cursor = ''); + queueMicrotask(() => bringLocationLayersToFront(map)); + return () => { map.off('click', 'clusters', handleClusterClick); map.off('click', 'unclustered-point-circle', handlePointClick); diff --git a/packages/ui/src/modules/places/gadm-picker/GadmPicker.tsx b/packages/ui/src/modules/places/gadm-picker/GadmPicker.tsx index a45431fa..5d972346 100644 --- a/packages/ui/src/modules/places/gadm-picker/GadmPicker.tsx +++ b/packages/ui/src/modules/places/gadm-picker/GadmPicker.tsx @@ -152,14 +152,20 @@ export function GadmPicker({ map, active, onClose, onSelectionChange, className const setupMapLayers = () => { if (!map.getStyle()) return; if (!map.getSource('gadm-picker-features')) { + // Draw GADM picker polygons below place clusters (clusters are added first; opening the sidebar after would otherwise stack GADM on top). + const beforeId = map.getLayer('clusters') + ? 'clusters' + : map.getLayer('unclustered-point-circle') + ? 'unclustered-point-circle' + : undefined; map.addSource('gadm-picker-features', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); - map.addLayer({ id: 'gadm-picker-fill', type: 'fill', source: 'gadm-picker-features', paint: { 'fill-color': ['get', '_fillColor'], 'fill-opacity': 0.15 } }); - map.addLayer({ id: 'gadm-picker-line', type: 'line', source: 'gadm-picker-features', paint: { 'line-color': ['get', '_lineColor'], 'line-width': ['case', ['==', ['get', 'isOuter'], true], 3, 1] } }); + map.addLayer({ id: 'gadm-picker-fill', type: 'fill', source: 'gadm-picker-features', paint: { 'fill-color': ['get', '_fillColor'], 'fill-opacity': 0.15 } }, beforeId); + map.addLayer({ id: 'gadm-picker-line', type: 'line', source: 'gadm-picker-features', paint: { 'line-color': ['get', '_lineColor'], 'line-width': ['case', ['==', ['get', 'isOuter'], true], 3, 1] } }, beforeId); updateMapFeatures(); if (!map.getSource('gadm-picker-highlight')) { map.addSource('gadm-picker-highlight', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); - map.addLayer({ id: 'gadm-picker-highlight-fill', type: 'fill', source: 'gadm-picker-highlight', paint: { 'fill-color': '#0ea5e9', 'fill-opacity': 0.25 } }); - map.addLayer({ id: 'gadm-picker-highlight-line', type: 'line', source: 'gadm-picker-highlight', paint: { 'line-color': '#0ea5e9', 'line-width': 2 } }); + map.addLayer({ id: 'gadm-picker-highlight-fill', type: 'fill', source: 'gadm-picker-highlight', paint: { 'fill-color': '#0ea5e9', 'fill-opacity': 0.25 } }, beforeId); + map.addLayer({ id: 'gadm-picker-highlight-line', type: 'line', source: 'gadm-picker-highlight', paint: { 'line-color': '#0ea5e9', 'line-width': 2 } }, beforeId); } } }; diff --git a/packages/ui/src/modules/places/gridsearch/GridSearchResults.tsx b/packages/ui/src/modules/places/gridsearch/GridSearchResults.tsx index d5ae34c0..17fb5daf 100644 --- a/packages/ui/src/modules/places/gridsearch/GridSearchResults.tsx +++ b/packages/ui/src/modules/places/gridsearch/GridSearchResults.tsx @@ -1,9 +1,9 @@ import React, { useState, useCallback, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { LayoutGrid, List, Map as MapIcon, PieChart, FileText, Terminal, PlusCircle, Loader2, Share2, Image as ImageIcon, Palette, PanelLeftClose, PanelLeftOpen, Merge, Pause, Play, Square } from 'lucide-react'; +import { LayoutGrid, List, Map as MapIcon, PieChart, FileText, Terminal, PlusCircle, Loader2, Share2, Image as ImageIcon, Palette, PanelLeftClose, PanelLeftOpen, Merge, Pause, Play, Square, SlidersHorizontal } from 'lucide-react'; -import { CompetitorsGridView } from '../PlacesGridView'; -import { PlacesMapView } from '../PlacesMapView'; +import { CompetitorsGridView, type CompetitorsGridViewHandle } from '../PlacesGridView'; +import { PlacesMapView, type MapFeatures } from '../PlacesMapView'; import { PlacesThumbView } from '../PlacesThumbView'; import { PlacesMetaView } from '../PlacesMetaView'; import { PlacesReportView } from './PlacesReportView'; @@ -64,6 +64,17 @@ import { ImportContactsDialog } from '../../contacts/ImportContactsDialog'; import { exportGridSearchToContacts } from '../client-gridsearch'; import { UserPlus } from 'lucide-react'; +/** URL/localStorage toggle for map layer visibility; same storage prefix as `useGridSearchState`. */ +function readGridsearchPersistentToggle(key: string, urlValue: string | null, defaultValue: boolean): boolean { + if (typeof window === 'undefined') return defaultValue; + if (urlValue !== null) { + localStorage.setItem(`gridsearch_${key}`, urlValue); + return urlValue === '1'; + } + const stored = localStorage.getItem(`gridsearch_${key}`); + if (stored !== null) return stored === '1'; + return defaultValue; +} export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes, updateExcludedTypes, liveAreas, liveRadii, liveNodes, liveScanner, stats, streaming, statusMessage, sseLogs, onExpandSubmitted, onMergeSubmitted, isOwner = false, isPublic, onTogglePublic, isSidebarOpen, onToggleSidebar }: GridSearchResultsProps) => { const { controlPaused, stats: streamStats, statusMessage: streamStatus } = useGridSearchStream(); @@ -105,8 +116,63 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes }); }, [competitors, excludedTypes]); - isOwner = true; const [searchParams, setSearchParams] = useSearchParams(); + + const showMapRegions = React.useMemo( + () => readGridsearchPersistentToggle('regions', searchParams.get('regions'), true), + [searchParams] + ); + + const mapCustomFeatures = React.useMemo((): Partial => { + const base: Partial = { showRegions: showMapRegions }; + if (!isOwner) { + base.enableSidebarTools = false; + base.enableSimulator = false; + base.enableInfoPanel = false; + } + return base; + }, [isOwner, showMapRegions]); + + const [viewMode, setViewMode] = useState(() => { + const urlView = searchParams.get('view') as ViewMode; + const allowed: ViewMode[] = ['grid', 'thumb', 'map', 'meta', 'report', 'poster', ...(import.meta.env.DEV ? (['log'] as const) : [])]; + if (urlView && allowed.includes(urlView)) { + if (urlView === 'log' && !isOwner) return 'grid'; + return urlView; + } + return 'grid'; + }); + + const [posterTheme, setPosterTheme] = useState(() => searchParams.get('theme') || 'terracotta'); + + /** When true, list view shows all rows and applies saved excludes as MUI column filters (editable in the filter panel). When false, rows are pre-filtered (previous behavior). */ + const [applySavedExcludesInGrid, setApplySavedExcludesInGrid] = useState(false); + /** Bumps on every Grid filters toggle so `CompetitorsGridView` remounts and drops in-memory filter state (types filter must not survive turning the mode off). */ + const [gridFilterRemountKey, setGridFilterRemountKey] = useState(0); + const gridExportRef = useRef(null); + + const toggleApplySavedExcludesInGrid = useCallback(() => { + setApplySavedExcludesInGrid((v) => !v); + setGridFilterRemountKey((k) => k + 1); + // Clear filter query params on both on and off: off avoids rehydrating e.g. `filter_types_excludesAnyOf` from the URL into the prefilter grid; on matches previous behavior. + setSearchParams((prev) => { + const p = new URLSearchParams(prev); + Array.from(p.keys()).forEach((k) => { + if (k.startsWith('filter_')) p.delete(k); + }); + p.delete('nofilter'); + return p; + }, { replace: true }); + }, [setSearchParams]); + + const getRowsMatchingCurrentFilters = useCallback(() => { + if (applySavedExcludesInGrid && viewMode === 'grid' && gridExportRef.current) { + const ids = new Set(gridExportRef.current.getFilteredPlaceIds()); + return competitors.filter((c) => ids.has(String((c as any).place_id || (c as any).placeId || (c as any).id))); + } + return filteredCompetitors; + }, [applySavedExcludesInGrid, viewMode, competitors, filteredCompetitors]); + const { state: restoredState } = useRestoredSearch(); const restoredGadmAreas = restoredState?.run?.request?.guided?.areas?.map((a: any) => ({ gid: a.gid, name: a.name, level: a.level, @@ -199,15 +265,6 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes } }, [jobId, mapSimSettings, onExpandSubmitted, discoverQuery]); - const [viewMode, setViewMode] = useState(() => { - const urlView = searchParams.get('view') as ViewMode; - if (urlView && ['grid', 'thumb', 'map', 'meta', 'report', 'poster', ...(import.meta.env.DEV ? ['log'] : [])].includes(urlView)) return urlView; - - return 'grid'; - }); - - const [posterTheme, setPosterTheme] = useState(() => searchParams.get('theme') || 'terracotta'); - // Selection and Sidebar Panel state const [selectedPlaceId, setSelectedPlaceId] = useState(null); const [showDetails, setShowDetails] = useState(false); @@ -270,6 +327,21 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes }, { replace: true }); }, [setSearchParams]); + /** Shared / anon viewers cannot use log view; normalize URL and state. */ + React.useEffect(() => { + if (isOwner) return; + if (searchParams.get('view') === 'log') { + setSearchParams((prev) => { + const p = new URLSearchParams(prev); + p.set('view', 'grid'); + return p; + }, { replace: true }); + } + if (viewMode === 'log') { + handleViewChange('grid'); + } + }, [isOwner, searchParams, setSearchParams, viewMode, handleViewChange]); + // Mock functions for now till we have real enrichment tracking in grid search results const dummyEnrich = async () => { }; const handleMapCenterUpdate = () => { }; @@ -487,7 +559,7 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes Report - {import.meta.env.DEV && sseLogs && ( + {isOwner && import.meta.env.DEV && sseLogs && (
-
+ {isOwner && ( + <> +
- {( + + + )} + + {isOwner && ( )} - - - - - - Export Results - - { - const q = restoredState?.run?.request?.search?.types?.[0] || restoredState?.run?.request?.types?.[0] || restoredState?.run?.request?.guided?.query || 'places'; - const r = restoredState?.run?.request?.search?.region || restoredState?.run?.request?.region || restoredState?.run?.request?.guided?.areas?.[0]?.name || ''; - const baseName = r ? `${q} @ ${r}` : q; - const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_'); - exportToCSV(filteredCompetitors, `${safeName}.csv`); - }}> - - Export as CSV - - { - const q = restoredState?.run?.request?.search?.types?.[0] || restoredState?.run?.request?.types?.[0] || restoredState?.run?.request?.guided?.query || 'places'; - const r = restoredState?.run?.request?.search?.region || restoredState?.run?.request?.region || restoredState?.run?.request?.guided?.areas?.[0]?.name || ''; - const baseName = r ? `${q} @ ${r}` : q; - const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_'); - exportToJSON(filteredCompetitors, `${safeName}.json`); - }}> - - Export as JSON - + {isOwner && ( + + + + + + Export Results + + { + const q = restoredState?.run?.request?.search?.types?.[0] || restoredState?.run?.request?.types?.[0] || restoredState?.run?.request?.guided?.query || 'places'; + const r = restoredState?.run?.request?.search?.region || restoredState?.run?.request?.region || restoredState?.run?.request?.guided?.areas?.[0]?.name || ''; + const baseName = r ? `${q} @ ${r}` : q; + const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_'); + exportToCSV(getRowsMatchingCurrentFilters(), `${safeName}.csv`); + }}> + + Export as CSV + + { + const q = restoredState?.run?.request?.search?.types?.[0] || restoredState?.run?.request?.types?.[0] || restoredState?.run?.request?.guided?.query || 'places'; + const r = restoredState?.run?.request?.search?.region || restoredState?.run?.request?.region || restoredState?.run?.request?.guided?.areas?.[0]?.name || ''; + const baseName = r ? `${q} @ ${r}` : q; + const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_'); + exportToJSON(getRowsMatchingCurrentFilters(), `${safeName}.json`); + }}> + + Export as JSON + - {import.meta.env.VITE_ENABLE_CONTACTS === 'true' && ( - <> - - setShowImportToContactsDialog(true)}> - - Import to Contacts - - - )} + {import.meta.env.VITE_ENABLE_CONTACTS === 'true' && ( + <> + + setShowImportToContactsDialog(true)}> + + Import to Contacts + + + )} - - + + + )} {isOwner && viewMode === 'poster' && ( <> @@ -576,7 +672,7 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes
)} - {isOwner ? ( + {isOwner && ( <>
- ) : isPublic ? ( - <> -
-
- -
- - ) : null} + )}
@@ -605,7 +691,9 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes
{viewMode === 'grid' && ( { })} @@ -614,6 +702,8 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes isOwner={isOwner} isPublic={isPublic} preset={(isPublic && !isOwner) || showDetails ? 'min' : 'full'} + seedExcludeTypeFilters={applySavedExcludesInGrid ? excludedTypes : undefined} + persistFiltersInUrl={!applySavedExcludesInGrid} /> )} @@ -628,6 +718,7 @@ export const GridSearchResults = React.memo(({ jobId, competitors, excludedTypes {(viewMode === 'map' || viewMode === 'poster') && ( { - return exportGridSearchToContacts(jobId, groupId); + if (viewMode === 'grid' && gridExportRef.current) { + const placeIds = gridExportRef.current.getFilteredPlaceIds(); + return exportGridSearchToContacts(jobId, groupId, { placeIds }); + } + return exportGridSearchToContacts(jobId, groupId, { excludedTypes }); }} /> diff --git a/packages/ui/src/modules/places/gridsearch/JobViewer.tsx b/packages/ui/src/modules/places/gridsearch/JobViewer.tsx index e46717e4..c4b8ab71 100644 --- a/packages/ui/src/modules/places/gridsearch/JobViewer.tsx +++ b/packages/ui/src/modules/places/gridsearch/JobViewer.tsx @@ -324,7 +324,8 @@ export const JobViewer = React.memo(({ jobId, isSidebarOpen, onToggleSidebar }: toast.success(translate("Search results merged!")); refetch(); }} - isOwner={jobId === restoredState?.run?.id && !!(window as any).isOwner} + isOwner={jobData?.isOwner === true || restoredState?.run?.isOwner === true} + isPublic={jobData?.isPublic === true || restoredState?.run?.isPublic === true || isSharingTarget} onTogglePublic={handleTogglePublic} isSidebarOpen={isSidebarOpen} onToggleSidebar={onToggleSidebar} diff --git a/packages/ui/src/modules/places/gridsearch/PlacesReportView.tsx b/packages/ui/src/modules/places/gridsearch/PlacesReportView.tsx index dd5dc91f..fdda56fa 100644 --- a/packages/ui/src/modules/places/gridsearch/PlacesReportView.tsx +++ b/packages/ui/src/modules/places/gridsearch/PlacesReportView.tsx @@ -71,7 +71,7 @@ export function PlacesReportView({ jobId }: { jobId: string }) { } return ( -
+

Grid Search Report