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

@djangocfg/ui-tools

v2.1.227

Published

Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps

Downloads

5,306

Readme

@djangocfg/ui-tools

Heavy React tools with lazy loading (React.lazy + Suspense).

No Next.js dependencies — works with Electron, Vite, CRA, and any React environment.

Part of DjangoCFG — modern Django framework for production-ready SaaS applications.

Install

pnpm add @djangocfg/ui-tools

Why ui-tools?

This package contains heavy components that are loaded lazily to keep your initial bundle small. Each tool is loaded only when used.

| Package | Use Case | |---------|----------| | @djangocfg/ui-core | Lightweight UI components (60+ components) | | @djangocfg/ui-tools | Heavy tools with lazy loading | | @djangocfg/ui-nextjs | Next.js apps (extends ui-core) |

Tools (12)

| Tool | Bundle Size | Description | |------|-------------|-------------| | Gallery | ~50KB | Image/video gallery with carousel, grid, lightbox | | Map | ~800KB | MapLibre GL maps with markers, clusters, layers | | Mermaid | ~800KB | Diagram rendering with declarative builders | | PrettyCode | ~500KB | Code syntax highlighting | | OpenapiViewer | ~400KB | OpenAPI schema viewer & playground | | JsonForm | ~300KB | JSON Schema form generator | | LottiePlayer | ~200KB | Lottie animation player | | AudioPlayer | ~200KB | Audio player with WaveSurfer.js | | VideoPlayer | ~150KB | Professional video player with Vidstack | | JsonTree | ~100KB | JSON visualization with modes (full/compact/inline) | | ImageViewer | ~50KB | Image viewer with zoom/pan/rotate/flip and gallery navigation | | CronScheduler | ~15KB | Cron expression builder with intuitive UI |

Tree-Shakeable Imports

For better bundle optimization, use subpath imports:

// Only loads Gallery (~50KB instead of full package)
import { Gallery, GalleryLightbox } from '@djangocfg/ui-tools/gallery';

// Only loads Map (~800KB)
import { MapContainer, MapMarker } from '@djangocfg/ui-tools/map';

// Mermaid builders (no heavy Mermaid dependency until render)
import { FlowDiagram, SequenceDiagram, JourneyDiagram } from '@djangocfg/ui-tools/mermaid';

Exports

| Path | Content | |------|---------| | @djangocfg/ui-tools | All tools with lazy loading | | @djangocfg/ui-tools/gallery | Gallery components & hooks | | @djangocfg/ui-tools/map | Map components & utilities | | @djangocfg/ui-tools/mermaid | Mermaid component & declarative builders | | @djangocfg/ui-tools/styles | CSS styles |

Gallery

Full-featured image/video gallery with carousel, grid view, and fullscreen lightbox.

import { Gallery } from '@djangocfg/ui-tools/gallery';

const images = [
  { id: '1', src: '/photo1.jpg', alt: 'Photo 1' },
  { id: '2', src: '/photo2.jpg', alt: 'Photo 2' },
  { id: '3', src: '/video.mp4', alt: 'Video', type: 'video' },
];

function PhotoGallery() {
  return (
    <Gallery
      images={images}
      previewMode="carousel"  // or "grid"
      showThumbnails
      enableLightbox
      aspectRatio={16 / 9}
    />
  );
}

Gallery Components

| Component | Description | |-----------|-------------| | Gallery | Complete gallery with carousel/grid + lightbox | | GalleryCompact | Minimal carousel for cards | | GalleryGrid | Grid layout with "show more" badge | | GalleryLightbox | Fullscreen lightbox viewer | | GalleryCarousel | Embla-based carousel | | GalleryThumbnails | Thumbnail strip navigation |

Gallery Hooks

| Hook | Description | |------|-------------| | useGallery | Gallery state management | | useSwipe | Touch swipe gestures | | usePinchZoom | Pinch-to-zoom for mobile | | usePreloadImages | Image preloading |

Map

MapLibre GL maps with React components for markers, clusters, popups, and custom layers.

import { MapContainer, MapMarker, MapPopup } from '@djangocfg/ui-tools/map';

const markers = [
  { id: '1', lat: 37.7749, lng: -122.4194, title: 'San Francisco' },
  { id: '2', lat: 34.0522, lng: -118.2437, title: 'Los Angeles' },
];

function LocationMap() {
  return (
    <MapContainer
      initialViewport={{ latitude: 36, longitude: -119, zoom: 5 }}
      style="streets"
    >
      {markers.map((m) => (
        <MapMarker key={m.id} latitude={m.lat} longitude={m.lng}>
          <MapPopup>{m.title}</MapPopup>
        </MapMarker>
      ))}
    </MapContainer>
  );
}

Map Components

| Component | Description | |-----------|-------------| | MapContainer | Main map container with controls | | MapMarker | Custom marker with React children | | MapPopup | Popup attached to marker | | MapCluster | Clustered markers with spiderfy | | MapSource / MapLayer | Custom GeoJSON layers | | MapControls | Navigation controls | | MapLegend | Map legend component | | LayerSwitcher | Toggle map layers | | DrawControl | Drawing tools (optional) | | GeocoderControl | Search/geocoding (optional) |

Overlapping Markers

When multiple markers are at the same location, use offsetOverlappingMarkers to spread them out before rendering:

import { MapCluster } from '@djangocfg/ui-tools/map';
import { offsetOverlappingMarkers } from '@djangocfg/ui-tools/map';

// Pre-process data to offset overlapping points
const processedData = useMemo(() => {
  const markers = geojson.features.map((f, i) => ({
    id: f.properties?.id || `point-${i}`,
    longitude: f.geometry.coordinates[0],
    latitude: f.geometry.coordinates[1],
    data: f.properties,
  }));

  const offsetMarkers = offsetOverlappingMarkers(markers, {
    spiralRadius: 0.0003, // ~30m spread
  });

  return {
    type: 'FeatureCollection',
    features: offsetMarkers.map((m) => ({
      type: 'Feature',
      properties: m.data,
      geometry: { type: 'Point', coordinates: [m.longitude, m.latitude] },
    })),
  };
}, [geojson]);

<MapCluster sourceId="points" data={processedData} />

| Utility | Description | |---------|-------------| | offsetOverlappingMarkers(markers, options) | Spread overlapping markers using Fermat spiral | | hasOverlappingMarkers(markers) | Check if any markers overlap | | getOverlapStats(markers) | Get statistics about overlapping markers |

Map Hooks

| Hook | Description | |------|-------------| | useMap | Access map instance | | useMapControl | Programmatic map control | | useMarkers | Marker management | | useMapEvents | Map event handlers | | useMapViewport | Viewport state | | useMapLayers | Layer management |

Map Styles

import { MAP_STYLES, getMapStyle } from '@djangocfg/ui-tools/map';

// Available styles: streets, satellite, dark, light, terrain
<MapContainer style="dark" />

Layer Utilities

import {
  createClusterLayers,
  createPointLayer,
  createPolygonLayer,
  createLineLayer,
} from '@djangocfg/ui-tools/map';

Video Player

import { VideoPlayer } from '@djangocfg/ui-tools';

<VideoPlayer
  src="https://example.com/video.mp4"
  poster="/thumbnail.jpg"
  autoplay={false}
/>

Audio Player

Three components for different use cases:

import {
  LazyHybridSimplePlayer,   // Full player: cover art + reactive effects + controls
  LazyHybridCompactPlayer,  // Single-row: play/pause + waveform + timer
  LazyHybridAudioPlayer,    // Controls only (requires HybridAudioProvider)
} from '@djangocfg/ui-tools';

// Full player with reactive cover art
<LazyHybridSimplePlayer
  src="https://example.com/audio.mp3"
  title="Track Title"
  reactiveCover
  variant="spotlight"
/>

// Compact single-row — for lists and tight spaces
<LazyHybridCompactPlayer
  src="https://example.com/audio.mp3"
  title="Track Title"
  autoPlay
/>

Mermaid Diagrams

Render Mermaid diagrams with fullscreen zoom support and type-safe declarative builders.

Basic Usage

import { LazyMermaid } from '@djangocfg/ui-tools';

<LazyMermaid chart={`
  graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action]
    B -->|No| D[End]
`} />

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | chart | string | - | Mermaid diagram syntax | | className | string | '' | Additional CSS classes | | isCompact | boolean | false | Compact rendering mode | | fullscreen | boolean | true | Enable fullscreen button with pinch-zoom |

Declarative Builders

Type-safe builders for creating Mermaid diagrams programmatically:

import { LazyMermaid } from '@djangocfg/ui-tools';
import {
  FlowDiagram,
  SequenceDiagram,
  JourneyDiagram,
  useStylePresets,
  useBoxColors,
} from '@djangocfg/ui-tools/mermaid';

function MyDiagram() {
  const presets = useStylePresets();
  const boxes = useBoxColors();

  // Flow diagram with type-safe nodes
  type Nodes = 'start' | 'check' | 'success' | 'finish';
  const flow = FlowDiagram<Nodes>({ direction: 'TB' });

  flow.node('start').rect('Start');
  flow.node('check').rhombus('Is it working?');
  flow.node('success').rect('Great!');
  flow.node('finish').stadium('End');

  flow.edge('start').to('check').solid();
  flow.edge('check').to('success').solid('Yes');
  flow.edge('check').to('finish').solid('No');
  flow.edge('success').to('finish').solid();

  // Apply theme-aware styles
  flow.style.define('success', presets.success);
  flow.style.apply('success', 'success', 'finish');

  return <LazyMermaid chart={flow.toString()} />;
}

Available Builders

| Builder | Description | |---------|-------------| | FlowDiagram<Nodes> | Flowcharts with nodes, edges, subgraphs | | SequenceDiagram | Sequence diagrams with participants, messages | | JourneyDiagram | User journey diagrams with sections, tasks |

Theme Hooks

| Hook | Description | |------|-------------| | useThemePalette() | Full palette from CSS variables | | useStylePresets() | Pre-built style configs (success, warning, etc.) | | useBoxColors() | Colors for sequence diagram boxes |

FlowDiagram API

const flow = FlowDiagram<'A' | 'B' | 'C'>({ direction: 'TB' });

// Nodes
flow.node('A').rect('Rectangle');
flow.node('B').round('Rounded');
flow.node('C').rhombus('Diamond');
flow.node('D').stadium('Stadium');
flow.node('E').cylinder('Database');
flow.node('F').hexagon('Hexagon');

// Edges
flow.edge('A').to('B').solid();
flow.edge('A').to('B').solid('with label');
flow.edge('A').to('B').dotted();
flow.edge('A').to('B').thick();

// Subgraphs
flow.subgraph('Group Name', (sub) => {
  sub.direction('LR');
  sub.node('X').rect('Inside');
});

// Styles
flow.style.define('myStyle', { fill: '#f00', stroke: '#000' });
flow.style.apply('myStyle', 'A', 'B');

SequenceDiagram API

Static API (type-safe, for known participants):

const { d, rect, alt, loop, toString } = SequenceDiagram({
  User: 'actor',
  App: 'participant',
  API: 'participant',
}, { autoNumber: true });

// Messages (type-safe chain)
d.User.sync.App.msg('Click button');
d.App.async.API.msg('Fetch data');
d.API.asyncReply.App.msg('Response');

// Blocks
rect('#rgba(0,100,200,0.2)', () => {
  d.User.sync.App.msg('Login');
});

alt('Success', () => {
  d.App.syncReply.User.msg('Welcome!');
}).else('Failure', () => {
  d.App.syncReply.User.msg('Error');
});

loop('Every 5s', () => {
  d.App.async.API.msg('Heartbeat');
});

return toString();

Dynamic API (for runtime participant names):

// Participants from API/database
const characters = ['Alice', 'Bob', 'Charlie'];
const participants: Record<string, 'participant'> = {};
characters.forEach(c => { participants[c] = 'participant'; });

const seq = SequenceDiagram(participants, { autoNumber: true });

// Dynamic methods - no type assertions needed
seq.message('Alice', 'Bob', 'Hello!');
seq.message('Bob', 'Alice', 'Hi!', 'syncReply');
seq.message('Alice', 'Charlie', 'Ping', 'async');

// Notes
seq.noteOver('Alice', 'Thinking...');
seq.noteOverSpan('Alice', 'Bob', 'Discussion');
seq.noteLeft('Charlie', 'Waiting');
seq.noteRight('Charlie', 'Done');

// Blocks work the same
seq.rect('rgba(100,200,255,0.2)', () => {
  seq.message('Alice', 'Bob', 'Secret message');
});

return seq.toString();

| Dynamic Method | Description | |----------------|-------------| | message(from, to, text, arrow?) | Send message (arrow: sync, syncReply, async, asyncReply, solid, dotted, cross) | | noteOver(participant, text) | Note over one participant | | noteOverSpan(p1, p2, text) | Note spanning two participants | | noteLeft(participant, text) | Note left of participant | | noteRight(participant, text) | Note right of participant |

JourneyDiagram API

const journey = JourneyDiagram({ title: 'User Onboarding' });

journey.section('Discovery')
  .task('Visit landing page', 5, 'User')
  .task('Read features', 4, 'User');

journey.section('Sign Up')
  .task('Click Sign Up', 5, 'User')
  .task('Fill form', 2, 'User')
  .task('Verify email', 4, ['User', 'System']);

return journey.toString();

Code Highlighting

import { PrettyCode } from '@djangocfg/ui-tools';

<PrettyCode
  code={`const hello = "world";`}
  language="typescript"
/>

JSON Form

import { JsonSchemaForm } from '@djangocfg/ui-tools';

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string', title: 'Name' },
    email: { type: 'string', format: 'email', title: 'Email' },
  },
};

<JsonSchemaForm
  schema={schema}
  onSubmit={(data) => console.log(data)}
/>

Components

| Component | Description | |-----------|-------------| | Markdown | Markdown renderer with GFM support |

Stores

| Store | Description | |-------|-------------| | useMediaCacheStore | Media caching for video/audio players |

Cron Scheduler

Compact cron expression builder with intuitive UI. Supports Daily, Weekly, Monthly schedules and custom cron expressions.

import { CronScheduler } from '@djangocfg/ui-tools';

<CronScheduler
  value="0 9 * * 1-5"
  onChange={(cron) => console.log(cron)}
  showPreview
  allowCopy
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | - | Cron expression (Unix 5-field format) | | onChange | (cron: string) => void | - | Callback when schedule changes | | defaultType | 'daily' \| 'weekly' \| 'monthly' \| 'custom' | 'daily' | Initial schedule type | | showPreview | boolean | true | Show human-readable preview | | showCronExpression | boolean | true | Show cron expression in preview | | allowCopy | boolean | false | Enable copy to clipboard | | timeFormat | '12h' \| '24h' | '24h' | Time display format | | disabled | boolean | false | Disable all interactions |

Context Hooks

For custom compositions, use the context hooks:

import {
  CronSchedulerProvider,
  useCronType,
  useCronTime,
  useCronWeekDays,
  useCronMonthDays,
  useCronPreview,
} from '@djangocfg/ui-tools';

Utilities

import {
  buildCron,    // State → Cron expression
  parseCron,    // Cron → State
  humanizeCron, // Cron → Human description
  isValidCron,  // Validate cron syntax
} from '@djangocfg/ui-tools';

Image Viewer

Image viewer with zoom, pan, rotate, flip and gallery navigation.

import { ImageViewer } from '@djangocfg/ui-tools';

// Single image
<div className="w-full h-[500px]">
  <ImageViewer
    images={[{ file: { name: 'photo.jpg', path: '/images/photo.jpg' }, src: '/images/photo.jpg' }]}
  />
</div>

// Gallery — pass multiple images, keyboard ←/→ navigation enabled automatically
<div className="w-full h-[500px]">
  <ImageViewer
    images={[
      { file: { name: 'Photo 1', path: 'p1' }, src: 'https://example.com/1.jpg' },
      { file: { name: 'Photo 2', path: 'p2' }, src: 'https://example.com/2.jpg' },
      { file: { name: 'Photo 3', path: 'p3' }, src: 'https://example.com/3.jpg' },
    ]}
    initialIndex={0}
  />
</div>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | images | ImageItem[] | required | Array of images to display | | initialIndex | number | 0 | Index of the image to show first | | inDialog | boolean | false | Hide expand button (for nested usage) |

Keyboard Shortcuts

| Key | Action | |-----|--------| | + / = | Zoom in | | - | Zoom out | | 0 | Reset to fit | | R | Rotate 90° | | | Previous image (gallery) | | | Next image (gallery) |

JSON Tree

JSON visualization with three display modes:

import { LazyJsonTree } from '@djangocfg/ui-tools';

// Full mode (default) - with toolbar (Expand All, Copy, Download)
<LazyJsonTree data={obj} mode="full" />

// Compact mode - no toolbar, subtle border
<LazyJsonTree data={obj} mode="compact" />

// Inline mode - minimal, no border, for embedding
<LazyJsonTree data={obj} mode="inline" />

| Mode | Toolbar | Border | Use Case | |------|---------|--------|----------| | full | Yes | Yes | Standalone viewer | | compact | No | Subtle | Cards, panels | | inline | No | No | Embedded in lists, logs |

Lazy Loading

All heavy tools have unified lazy-loaded versions with built-in Suspense fallbacks:

import {
  LazyMapContainer,      // ~800KB
  LazyMermaid,           // ~800KB
  LazyPrettyCode,        // ~500KB
  LazyOpenapiViewer,     // ~400KB
  LazyJsonSchemaForm,    // ~300KB
  LazyLottiePlayer,      // ~200KB
  LazyHybridAudioPlayer,        // ~200KB — controls only
  LazyHybridSimplePlayer,       // ~200KB — full player with cover & effects
  LazyHybridCompactPlayer,      // ~200KB — compact single-row player
  LazyVideoPlayer,       // ~150KB
  LazyJsonTree,          // ~100KB
  LazyImageViewer,       // ~50KB
  LazyCronScheduler,     // ~15KB
} from '@djangocfg/ui-tools';

// Just use them - no Suspense wrapper needed!
<LazyMermaid chart={diagram} />
<LazyMapContainer initialViewport={viewport} />
<LazyVideoPlayer src="/video.mp4" />

Custom Lazy Components

Create your own lazy components with createLazyComponent:

import { createLazyComponent, CardLoadingFallback } from '@djangocfg/ui-tools';

const LazyMyComponent = createLazyComponent(
  () => import('./MyHeavyComponent'),
  {
    displayName: 'LazyMyComponent',
    fallback: <CardLoadingFallback title="Loading..." minHeight={200} />,
  }
);

Loading Fallbacks

Built-in fallback components for different use cases:

| Component | Use Case | |-----------|----------| | LoadingFallback | Generic spinner with optional text | | CardLoadingFallback | Card-styled loading with title | | MapLoadingFallback | Map-specific with location icon | | Spinner | Simple spinning loader | | LazyWrapper | Suspense wrapper with configurable fallback |

Requirements

  • React >= 18 or >= 19
  • Tailwind CSS >= 4
  • Zustand >= 5
  • @djangocfg/ui-core (peer dependency)

Optional Dependencies (for Map)

# For drawing tools
pnpm add @mapbox/mapbox-gl-draw

# For geocoding/search
pnpm add @maplibre/maplibre-gl-geocoder

License

MIT