npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

neogestify-ui-components

v2.1.0

Published

Biblioteca de componentes UI reutilizables con React, Tailwind y SweetAlert, con VenueMapEditor o editor de mapas basico

Downloads

573

Readme

UI Components

Biblioteca de componentes UI reutilizables con React, Tailwind CSS y SweetAlert2.

Características

  • Componentes HTML preestilizados (Button, Input, Form, Select, Table, Modal)
  • Colección de iconos SVG
  • Alertas preconfiguradas con SweetAlert2
  • Soporte para modo claro/oscuro
  • TypeScript incluido
  • Compatible con Tailwind CSS 4.1

Instalación

Si estás usando workspaces con npm/bun:

NPM

# En tu proyecto
npm i neogestify-ui-components

BUN

# En tu proyecto
npm i neogestify-ui-components

Configuración

1. Asegúrate de tener Tailwind CSS configurado en tu proyecto

bun add -D [email protected]

Tu proyecto debe tener Tailwind configurado ya que los componentes solo usan clases de Tailwind (no incluyen CSS compilado).

2. Configura Tailwind para escanear los componentes de la biblioteca

⚠️ IMPORTANTE: Esta librería requiere que configures Tailwind para escanear sus archivos fuente.

Para Tailwind CSS v4:

En tu archivo CSS principal (por ejemplo src/index.css):

@import "tailwindcss";

@source "../node_modules/neogestify-ui-components/src";

Agrega este script a tu index.html

<script>
      // Prevenir flash de contenido sin estilo (FOUC)
      const theme = localStorage.getItem('theme') || 'light';
      if (theme === 'dark') {
        document.documentElement.classList.add('dark');
      } else {
        document.documentElement.classList.remove('dark');
      }
</script>

3. Instala las dependencias peer

bun add react react-dom sweetalert2 sweetalert2-react-content

Uso

La biblioteca está organizada en módulos independientes:

Componentes HTML

import { Button, Input, Form, Select, Table, Modal } from 'neogestify-ui-components/html';

function MiComponente() {
  return (
    <Form onSubmit={handleSubmit}>
      <Input
        label="Nombre"
        placeholder="Tu nombre"
        value={nombre}
        onChange={(e) => setNombre(e.target.value)}
      />

      <Select
        label="País"
        options={[
          { value: 'mx', label: 'México' },
          { value: 'ar', label: 'Argentina' }
        ]}
      />

      <Button variant="primary" type="submit">
        Enviar
      </Button>
    </Form>
  );
}

Iconos

import {
  HomeIcon,
  SaveIcon,
  DeleteIcon,
  EditIcon
} from 'neogestify-ui-components/icons';

function MiComponente() {
  return (
    <div>
      <HomeIcon className="w-6 h-6 text-blue-500" />
      <SaveIcon className="w-5 h-5 text-green-600" />
    </div>
  );
}

Alertas

import {
  AlertaExito,
  AlertaError,
  AlertaAdvertencia,
  AlertaConfirmacion,
  AlertaToast
} from 'neogestify-ui-components/alerts';

function MiComponente() {
  const handleGuardar = async () => {
    try {
      await guardarDatos();
      AlertaExito('¡Guardado!', 'Los datos se guardaron correctamente');
    } catch (error) {
      AlertaError('Error', 'No se pudieron guardar los datos');
    }
  };

  const handleEliminar = () => {
    AlertaAdvertencia(
      '¿Estás seguro?',
      'Esta acción no se puede deshacer',
      async () => {
        await eliminarDatos();
        AlertaToast('Eliminado', 'Registro eliminado', 'success');
      }
    );
  };

  return (
    <Button variant="danger" onClick={handleEliminar}>
      Eliminar
    </Button>
  );
}

Sistema de Tema

El sistema de tema incluye un Context Provider y un componente toggle listo para usar.

1. Configurar el ThemeProvider

Envuelve tu aplicación con el ThemeProvider:

// main.tsx o App.tsx
import { ThemeProvider } from 'neogestify-ui-components/theme';

function Main() {
  return (
    <ThemeProvider>
      <App />
    </ThemeProvider>
  );
}

2. Usar el ThemeToggle

import { ThemeToggle } from 'neogestify-ui-components/theme';

function Header() {
  return (
    <nav>
      <ThemeToggle />
    </nav>
  );
}

3. Usar el hook useTheme

import { useTheme } from 'neogestify-ui-components/theme';

function MiComponente() {
  const { theme, toggleTheme, setTheme } = useTheme();

  return (
    <div>
      <p>Tema actual: {theme}</p>
      <button onClick={toggleTheme}>Cambiar tema</button>
      <button onClick={() => setTheme('dark')}>Modo oscuro</button>
      <button onClick={() => setTheme('light')}>Modo claro</button>
    </div>
  );
}

El tema se guarda automáticamente en localStorage y se aplica al cargar la página.

Componentes Disponibles

Button

Variantes: primary, secondary, danger, success, warning, outline, icon, nav, link, toggle

<Button variant="primary" isLoading loadingText="Guardando...">
  Guardar
</Button>

Input

Soporta tipos: text, email, password, number, checkbox, etc.

<Input
  label="Email"
  type="email"
  error="Email inválido"
  helperText="Ingresa tu correo electrónico"
/>

Select

<Select
  label="Categoría"
  placeholder="Selecciona..."
  options={categorias}
  variant="default"
/>

Table

<Table
  headers={['ID', 'Nombre', 'Email']}
  rows={[
    ['1', 'Juan', '[email protected]'],
    ['2', 'María', '[email protected]']
  ]}
/>

Modal

const modalRef = useRef<ModalRef>(null);

<Modal
  ref={modalRef}
  title="Mi Modal"
  onClose={() => setShowModal(false)}
  footer={
    <Button onClick={() => modalRef.current?.handleClose()}>
      Cerrar
    </Button>
  }
>
  <p>Contenido del modal</p>
</Modal>

VenueMapEditor

Editor de mapas de recintos interactivo basado en SVG puro. Permite diseñar la planta de cualquier espacio (restaurantes, parqueaderos, estadios, oficinas, eventos, etc.) con herramientas de dibujo de paredes, colocación de objetos, múltiples plantas y sistema de librerías de elementos personalizados.

Importación

import {
  VenueMapEditor,  // editor completo
  VenueMapViewer,  // modo solo lectura
} from 'neogestify-ui-components/VenueMapEditor';

// Tipos TypeScript
import type {
  VenueMap, Floor, MapElement,
  ElementTypeDef, ElementGroup, ElementLibrary,
  ElementStatus, VenueMapEditorProps,
} from 'neogestify-ui-components/VenueMapEditor';

Uso básico

El componente funciona sin ninguna prop — crea un mapa vacío con una planta por defecto:

<VenueMapEditor />

Con configuración mínima:

<VenueMapEditor
  width="100%"
  height="700px"
  onChange={(map) => console.log('Mapa actualizado:', map)}
/>

Cargar y guardar un mapa desde código

El prop initialMap acepta un VenueMap (del estado de la app, de una API, de localStorage, etc.). Cuando el valor cambia por referencia, el editor reinicia su historial al nuevo mapa. El ciclo onChange → initialMap es seguro — el componente detecta el eco de su propio onChange y no genera bucles infinitos.

import { useState, useEffect } from 'react';
import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
import type { VenueMap } from 'neogestify-ui-components/VenueMapEditor';

function App() {
  const [map, setMap] = useState<VenueMap | undefined>();

  // Carga asíncrona desde API
  useEffect(() => {
    fetch('/api/maps/1')
      .then(r => r.json())
      .then(setMap);
  }, []);

  // Guarda automáticamente en cada cambio
  const handleChange = (updated: VenueMap) => {
    setMap(updated);
    fetch('/api/maps/1', {
      method: 'PUT',
      body: JSON.stringify(updated),
    });
  };

  return (
    <VenueMapEditor
      initialMap={map}
      onChange={handleChange}
      height="600px"
    />
  );
}

Props

| Prop | Tipo | Default | Descripción | |------|------|---------|-------------| | initialMap | VenueMap | mapa vacío | Mapa inicial. Se puede actualizar desde fuera para recargar el editor. | | onChange | (map: VenueMap) => void | — | Se llama en cada cambio del estado interno. | | domainConfigs | DomainConfig[] | [] | Array de catálogos de tipos predefinidos. Cada uno aparece como una pestaña separada en la paleta. | | domainConfig | DomainConfig | — | Obsoleto — usa domainConfigs. Catálogo único (se convierte internamente a un array de un elemento). | | libraryStorageKey | string | 'venueMapEditor:libraries' | Clave de localStorage donde se persisten las librerías importadas. Pasa '' para deshabilitar la persistencia. | | width | string \| number | '100%' | Ancho del componente. | | height | string \| number | '600px' | Alto del componente. | | gridSize | number | 20 | Tamaño de la cuadrícula en unidades de canvas. | | showGrid | boolean | true | Mostrar/ocultar cuadrícula al iniciar. | | snapToGrid | boolean | false | Activar snap de elementos a la cuadrícula. | | readOnly | boolean | false | Modo lectura: no se puede editar pero sí hacer pan/zoom. | | fixed | boolean | false | Igual que readOnly pero además oculta la barra de herramientas. Pensado para el viewer en producción. | | elementStatus | ElementStatus[] | — | Array de estados visuales por elemento (libre, ocupado, reservado, deshabilitado). | | onElementClick | (el: MapElement) => void | — | Callback genérico al hacer click en cualquier elemento (en modo viewer). | | onElementTypeClick | Record<string, (el: MapElement) => void> | — | Callbacks por tipo de elemento. El tipo específico tiene prioridad sobre onElementClick. |


Modo Viewer

VenueMapViewer es un alias de VenueMapEditor con fixed={true}. Úsalo para mostrar el mapa en producción con elementos interactivos:

import { VenueMapViewer } from 'neogestify-ui-components/VenueMapEditor';
import type { ElementStatus } from 'neogestify-ui-components/VenueMapEditor';

const estados: ElementStatus[] = [
  { elementId: 'mesa-1', status: 'occupied' },
  { elementId: 'mesa-2', status: 'free' },
  { elementId: 'mesa-3', status: 'reserved' },
  { elementId: 'spot-4', status: 'disabled' },
];

<VenueMapViewer
  initialMap={myMap}
  elementStatus={estados}
  onElementTypeClick={{
    // El key es el `id` del tipo definido en la librería JSON
    TABLE_ROUND: (el) => abrirReserva(el.id),
    TABLE_RECT:  (el) => abrirReserva(el.id),
    PARKING_SPOT:(el) => asignarEspacio(el.id),
  }}
  // Fallback para tipos sin handler específico
  onElementClick={(el) => console.log('click en', el.type, el.id)}
/>

Colores de estado:

| status | Color | |----------|-------| | free | Verde claro | | occupied | Rojo claro | | reserved | Amarillo | | disabled | Gris |


Múltiples catálogos de elementos (domainConfigs)

Pasa varios DomainConfig vía la prop domainConfigs. Cada catálogo aparece como una pestaña separada en la paleta — los tipos nunca se mezclan entre tabs.

import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
import type { DomainConfig } from 'neogestify-ui-components/VenueMapEditor';

const mobiliario: DomainConfig = {
  id: 'furniture',
  name: 'Mobiliario',
  elementTypes: [
    { id: 'CHAIR',      label: 'Silla',        shape: 'circle', defaultWidth: 30,  defaultHeight: 30,  color: '#fef3c7', strokeColor: '#d97706' },
    { id: 'TABLE_RECT', label: 'Mesa rect.',   shape: 'rect',   defaultWidth: 100, defaultHeight: 60,  color: '#fef3c7', strokeColor: '#d97706' },
  ],
};

const iluminacion: DomainConfig = {
  id: 'lighting',
  name: 'Iluminación',
  elementTypes: [
    { id: 'SPOT_LIGHT', label: 'Foco',   shape: 'circle', defaultWidth: 40, defaultHeight: 40, color: '#fef9c3', strokeColor: '#ca8a04' },
    { id: 'STRIP_LIGHT', label: 'Tira LED', shape: 'rect', defaultWidth: 120, defaultHeight: 15, color: '#fef9c3', strokeColor: '#ca8a04' },
  ],
};

<VenueMapEditor domainConfigs={[mobiliario, iluminacion]} />

La paleta mostrará:

[ Mobiliario ]  [ Iluminación ]
─────────────────────────────
  [Silla]  [Mesa rect.]

Crear una librería de elementos (JSON)

Los elementos que aparecen en la paleta también se pueden definir en archivos JSON que el usuario carga desde el botón (Cargar librería).

Persistencia automática: las librerías importadas se guardan en localStorage bajo la clave libraryStorageKey (por defecto 'venueMapEditor:libraries'). Al recargar la página se restauran automáticamente antes de que el mapa renderice, evitando errores de "tipo de elemento desconocido".

Merge inteligente al importar: si un grupo con el mismo id ya existe, se añaden únicamente los elementos cuyo id no esté duplicado. Los elementos existentes nunca se sobreescriben.

// Cambiar la clave de almacenamiento (útil con múltiples editores en la misma app)
<VenueMapEditor libraryStorageKey="mi-proyecto:libs" />

// Deshabilitar persistencia
<VenueMapEditor libraryStorageKey="" />

Formato del JSON

{
  "grupoDeMesas": {
    "name": "Mesas de restaurante",
    "objects": [
      {
        "id": "TABLE_ROUND_2",
        "label": "Mesa 2 pers.",
        "shape": "circle",
        "defaultWidth": 60,
        "defaultHeight": 60,
        "color": "#fef3c7",
        "strokeColor": "#d97706"
      },
      {
        "id": "TABLE_RECT_4",
        "label": "Mesa 4 pers.",
        "shape": "rect",
        "defaultWidth": 110,
        "defaultHeight": 70,
        "color": "#fef3c7",
        "strokeColor": "#d97706"
      }
    ]
  },
  "infraestructura": {
    "name": "Infraestructura",
    "objects": [
      {
        "id": "PILLAR",
        "label": "Columna",
        "shape": "circle",
        "defaultWidth": 25,
        "defaultHeight": 25,
        "color": "#e5e7eb",
        "strokeColor": "#6b7280"
      },
      {
        "id": "ENTRANCE",
        "label": "Entrada",
        "shape": "arrow",
        "defaultWidth": 80,
        "defaultHeight": 30,
        "color": "#dcfce7",
        "strokeColor": "#16a34a"
      }
    ]
  }
}

Formas personalizadas SVG (shape: "path")

Ahora puedes definir cualquier figura SVG usando un path:

{
  "mi_libreria": {
    "name": "Mi librería",
    "objects": [
      {
        "id": "STAR",
        "label": "Estrella",
        "shape": "path",
        "svgPath": "M50 5 L61 35 L95 35 L68 57 L79 91 L50 70 L21 91 L32 57 L5 35 L39 35 Z",
        "viewBox": "0 0 100 100",
        "defaultWidth": 60,
        "defaultHeight": 60,
        "color": "#facc15",
        "strokeColor": "#ca8a04"
      }
    ]
  }
}

Propiedades para shape: "path":

| Campo | Tipo | Default | Descripción | |-------|------|---------|-------------| | svgPath | string | requerido | El atributo d del elemento <path> SVG | | viewBox | string | "0 0 100 100" | Espacio de coordenadas del path (formato: "minX minY width height") |

Nota: El path se escala automáticamente para llenar el bounding box width × height del elemento. El strokeWidth se compensa por el factor de escala para que sea visualmente consistente con los otros shapes.

Propiedades de cada objeto

| Campo | Tipo | Requerido | Descripción | |-------|------|-----------|-------------| | id | string | ✓ | Identificador único del tipo. Se usa como key en onElementTypeClick. | | label | string | ✓ | Nombre visible en la paleta y en el canvas. | | shape | "rect" \| "circle" \| "arrow" \| "path" | ✓ | Forma del objeto. | | defaultWidth | number | ✓ | Ancho inicial al colocar el elemento (unidades de canvas ≈ px a zoom 1×). | | defaultHeight | number | ✓ | Alto inicial. | | color | string | ✓ | Color de relleno (cualquier valor CSS: #hex, rgb(), hsl(), etc.). | | strokeColor | string | ✓ | Color del borde. | | svgPath | string | solo para shape:"path" | Atributo d de un <path> SVG. Se escala automáticamente al bounding box del elemento. | | viewBox | string | — | Espacio de coordenadas del svgPath. Formato: "minX minY w h". Default: "0 0 100 100". | | fillRule | "nonzero" \| "evenodd" | — | Regla de relleno SVG. Usa "evenodd" para crear huecos con sub-paths (engranajes, letras, donuts). Default: "nonzero". |

Formas disponibles

| shape | Descripción | Caso de uso típico | |---------|-------------|-------------------| | rect | Rectángulo | Mesas, espacios de parqueo, habitaciones | | circle | Elipse (círculo si width === height) | Mesas redondas, columnas, plantas | | arrow | Flecha apuntando a la derecha | Entradas, salidas, sentidos de circulación | | path | Forma SVG personalizada libre | Cualquier figura: estrella, engranaje, piano, logo... |

Formas personalizadas con shape: "path"

El campo svgPath acepta el atributo d de cualquier <path> SVG estándar. El sistema escala la figura para que ocupe exactamente el bounding box width × height del elemento. Puedes diseñar tus formas con Inkscape, Figma u otro editor vectorial y copiar el d= directamente.

{
  "especiales": {
    "name": "Especiales",
    "objects": [
      {
        "id": "STAR",
        "label": "Estrella",
        "shape": "path",
        "viewBox": "0 0 100 100",
        "svgPath": "M50 5 L61 35 L95 35 L68 57 L79 91 L50 70 L21 91 L32 57 L5 35 L39 35 Z",
        "defaultWidth": 60,
        "defaultHeight": 60,
        "color": "#facc15",
        "strokeColor": "#ca8a04"
      },
      {
        "id": "GEAR",
        "label": "Engranaje",
        "shape": "path",
        "viewBox": "0 0 100 100",
        "fillRule": "evenodd",
        "svgPath": "M36.61,17.66 L44.13,5.39 L55.87,5.39 L63.39,17.66 A35,35 0 0,1 77.40,14.30 L85.70,22.60 L82.34,36.61 A35,35 0 0,1 94.61,44.13 L94.61,55.87 L82.34,63.39 A35,35 0 0,1 85.70,77.40 L77.40,85.70 L63.39,82.34 A35,35 0 0,1 55.87,94.61 L44.13,94.61 L36.61,82.34 A35,35 0 0,1 22.60,85.70 L14.30,77.40 L17.66,63.39 A35,35 0 0,1 5.39,55.87 L5.39,44.13 L17.66,36.61 A35,35 0 0,1 14.30,22.60 L22.60,14.30 Z M65,50 A15,15 0 1,0 35,50 A15,15 0 1,0 65,50 Z",
        "defaultWidth": 70,
        "defaultHeight": 70,
        "color": "#94a3b8",
        "strokeColor": "#334155"
      }
    ]
  }
}

Hitbox de piso: para formas personalizadas que no llenan su bounding box (estrellas, logos, etc.), la detección de bordes usa un cuadrado de lado min(width, height) centrado en el elemento — esto evita que la figura quede demasiado restringida al área del piso.

Varios grupos en un archivo

Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece como una pestaña separada en la paleta. Se pueden cargar múltiples archivos — los grupos se acumulan. Cada grupo importado muestra un botón × en su pestaña para eliminarlo.

{
  "sillas":     { "name": "Sillas y asientos", "objects": [ ... ] },
  "servicio":   { "name": "Zona de servicio",  "objects": [ ... ] },
  "decoracion": { "name": "Decoración",         "objects": [ ... ] }
}

Librería de ejemplo — Parqueadero

{
  "spots": {
    "name": "Espacios",
    "objects": [
      { "id": "SPOT",        "label": "Normal",       "shape": "rect",   "defaultWidth": 60,  "defaultHeight": 120, "color": "#dbeafe", "strokeColor": "#3b82f6" },
      { "id": "SPOT_DISCAP", "label": "Discapacidad", "shape": "rect",   "defaultWidth": 80,  "defaultHeight": 120, "color": "#dcfce7", "strokeColor": "#22c55e" },
      { "id": "SPOT_EV",     "label": "Carga EV",     "shape": "rect",   "defaultWidth": 65,  "defaultHeight": 120, "color": "#d1fae5", "strokeColor": "#059669" },
      { "id": "SPOT_MOTO",   "label": "Moto",         "shape": "rect",   "defaultWidth": 35,  "defaultHeight": 75,  "color": "#fef9c3", "strokeColor": "#eab308" }
    ]
  },
  "circulacion": {
    "name": "Circulación",
    "objects": [
      { "id": "ENTRANCE", "label": "Entrada", "shape": "arrow", "defaultWidth": 85, "defaultHeight": 35, "color": "#dcfce7", "strokeColor": "#16a34a" },
      { "id": "EXIT",     "label": "Salida",  "shape": "arrow", "defaultWidth": 85, "defaultHeight": 35, "color": "#fee2e2", "strokeColor": "#dc2626" },
      { "id": "LANE",     "label": "Carril",  "shape": "rect",  "defaultWidth": 300,"defaultHeight": 60,  "color": "#f3f4f6", "strokeColor": "#9ca3af" }
    ]
  }
}

Modelo de datos TypeScript

El estado del editor se serializa en un objeto VenueMap. Puedes guardarlo en tu base de datos como JSON y restaurarlo con initialMap.

VenueMap
├── id: string
├── name: string
├── libraries?: ElementLibrary          ← librerías importadas (embebidas en el mapa)
└── floors: Floor[]
    ├── id: string
    ├── name: string
    ├── order: number
    ├── area: FloorArea                 ← forma del piso (rect | polygon)
    │   ├── shape: 'rect' | 'polygon'
    │   ├── x?, y?, width?, height?    ← para shape: 'rect'
    │   └── points?: [number,number][] ← para shape: 'polygon'
    ├── wallNodes: WallNode[]           ← vértices del grafo de paredes
    ├── walls: Wall[]                   ← segmentos de pared con grosor y material
    └── elements: MapElement[]
        ├── id: string
        ├── type: string               ← id del ElementTypeDef de la librería
        ├── x, y, width, height: number
        ├── rotation: number           ← grados
        ├── label?: string
        └── metadata?: Record<string, unknown>  ← datos propios de tu app

El campo metadata en MapElement está disponible para que cada app guarde datos propios por elemento (ej. ID de reserva, capacidad, propietario, estado personalizado).

// Ejemplo: guardar datos de negocio en metadata al crear elementos
const handleClick = (el: MapElement) => {
  // El metadata lo pone tu app, no el editor
  const reservaId = el.metadata?.reservaId as string;
  abrirModal(reservaId);
};

Herramientas del editor

| Tecla | Herramienta | Función | |-------|-------------|---------| | V | Seleccionar | Mover, redimensionar y rotar elementos. Arrastra el fondo del piso para moverlo. | | H | Desplazar | Pan del canvas con click izquierdo. | | W | Pared | Click fija el inicio; siguiente click termina el segmento (encadenado). Click derecho cancela. | | P | Colocar | Click en el piso coloca el elemento seleccionado en la paleta. | | E | Borrar | Click sobre un elemento o pared los elimina. | | Esc | — | Vuelve a Seleccionar. | | Ctrl+Z / Y | — | Deshacer / Rehacer. | | Ctrl+D | — | Duplicar selección. | | Del / Backspace | — | Eliminar selección. | | + / - | — | Zoom in / out. | | Rueda ratón | — | Zoom centrado en el cursor. | | Click medio + drag | — | Pan del canvas en cualquier modo. |


Gestión de plantas

La barra de pestañas (visible incluso en viewer) permite:

  • Click → cambiar de planta activa
  • Doble click en el nombre → renombrar en línea
  • ◀ ▶ → reordenar la planta activa
  • × → eliminar la planta (mínimo 1)
  • + → añadir nueva planta

Forma del piso (Rect vs Polígono)

El botón Rect / Poly de la barra de herramientas alterna entre:

  • Rect: rectángulo con 8 handles de redimensión en los bordes y esquinas.
  • Poly: polígono libre. Arrastra los vértices (cuadrados azules). Click en el diamante central de una arista añade un vértice. Doble-click en un vértice lo elimina (mínimo 3).

Los elementos y paredes siempre se mantienen dentro del piso al moverlos o colocarlos.


Exportar / Importar el mapa

| Botón | Función | |-------|---------| | ⬇ Exportar mapa | Descarga el estado actual como .json (incluye las librerías embebidas para portabilidad). | | ⬆ Importar mapa | Carga un .json exportado previamente, reemplazando el mapa actual. | | ⊞ Cargar librería | Carga un .json de elementos. Los grupos se añaden a la paleta como nuevas pestañas. Si el grupo ya existe, sólo se añaden los objetos con id nuevo (sin sobrescribir). La librería se persiste automáticamente en localStorage. |


Showcase / Demo

cd showcase
bun install
bun dev

Abre http://localhost:5173 en tu navegador.

Desarrollo

Build

bun install
bun run build

Estructura del proyecto

ui-components/
├── src/
│   ├── components/
│   │   ├── html/        # Componentes HTML
│   │   ├── icons/       # Iconos SVG
│   │   └── alerts/      # Alertas SweetAlert2
│   └── types/           # Tipos TypeScript
├── showcase/            # Demo/Showcase
└── dist/                # Build output

Modo Oscuro

Los componentes soportan modo oscuro automáticamente usando las clases dark: de Tailwind. Asegúrate de configurar el modo oscuro en tu proyecto:

// tailwind.config.js
export default {
  darkMode: 'class', // o 'media'
  // ...
}

Para activar el modo oscuro:

// Agregar/quitar la clase 'dark' en el html
document.documentElement.classList.add('dark');

Licencia

MIT