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

nn-widgets

v0.1.20

Published

Expo config plugin for adding native widgets (iOS WidgetKit & Android App Widgets)

Readme

nn-widgets

Expo config plugin for native widgets — iOS WidgetKit & Android App Widgets.

Supports four widget types:

| Type | Description | Data Source | | ---------- | ------------------------------------------------------- | --------------------------- | | "image" | Full-bleed background image | Static asset at build time | | "single" | Icon + Title + Subtitle + Value | updateWidget() at runtime | | "list" | Vertical list of items with icon/title/description | updateWidget() at runtime | | "grid" | Grid of items, each cell with icon on top + title below | updateWidget() at runtime |


Installation

npx expo install nn-widgets

Quick Start

1. Configure in app.json

{
  "plugins": [
    [
      "nn-widgets",
      {
        "widgets": [
          {
            "name": "QuickActions",
            "displayName": "Quick Actions",
            "type": "list",
            "deepLinkUrl": "myapp://widget",
            "widgetFamilies": ["systemMedium", "systemLarge"],
            "fallbackTitle": "Quick Actions",
            "fallbackSubtitle": "Open app to set up",
            "style": {
              "backgroundColor": "#1E1E2E",
              "titleColor": "#CDD6F4",
              "subtitleColor": "#A6ADC8",
              "accentColor": "#F38BA8"
            }
          }
        ],
        "ios": {
          "devTeam": "YOUR_TEAM_ID",
          "useAppGroups": true
        }
      }
    ]
  ]
}

2. Push data from your app

import { NNWidgets } from "nn-widgets";

await NNWidgets.updateWidget("QuickActions", {
  items: [
    {
      icon: "star.fill",
      title: "Morning Focus",
      description: "25 min session",
      deepLink: "myapp://session/123",
    },
    {
      icon: { url: "brain", size: 28, radius: 14, backgroundColor: "#3B3B5C" },
      title: { text: "Brainstorm", color: "#F38BA8", fontWeight: "bold" },
      description: { text: "15 min", fontSize: "caption" },
    },
  ],
});

3. Rebuild

npx expo prebuild --clean
npx expo run:ios

Plugin Configuration

widgets[] — Per-widget config

| Property | Type | Default | Description | | ------------------ | ------------------------------- | ---------------------------------------------- | ------------------------------------------------- | | name | string | required | Unique identifier (valid Swift/Kotlin identifier) | | displayName | string | required | Name shown in widget gallery | | description | string | "A widget for your app" | Description in widget gallery | | type | "single" \| "list" \| "image" | auto-detect | Widget display type | | deepLinkUrl | string | — | URL opened when widget is tapped | | showAppIcon | boolean | true | Show app icon in widget UI | | widgetFamilies | string[] | ["systemSmall","systemMedium","systemLarge"] | Supported sizes (iOS) | | fallbackTitle | string | displayName | Title shown when no data set | | fallbackSubtitle | string | "Open app to start" | Subtitle shown when no data set | | image | object \| string | — | Background image paths (image type) | | icons | Record<string, string> | — | Bundled icon images (name → path) | | style | object | — | Widget-level colors | | android | object | — | Android-specific overrides |

type auto-detection

  • If image prop is set → "image"
  • Otherwise → "single"
  • Set type: "list" explicitly for list widgets
  • Set type: "grid" explicitly for grid widgets

gridLayout (for type "grid")

Format: "COLUMNSxROWS" — controls the grid dimensions.

{ "gridLayout": "3x2" }

| Value | Columns | Rows | Max Items | | ------- | ------- | ---- | --------- | | "2x2" | 2 | 2 | 4 | | "3x2" | 3 | 2 | 6 | | "4x2" | 4 | 2 | 8 | | "2x3" | 2 | 3 | 6 |

If omitted, columns auto-adjust based on widget family:

  • systemSmall: 2 columns
  • systemMedium: 4 columns
  • systemLarge: 4 columns

image (for type "image")

{
  "image": {
    "small": "./assets/widgets/bg-small.png",
    "medium": "./assets/widgets/bg-medium.png",
    "large": "./assets/widgets/bg-large.png"
  }
}

Or a single image for all sizes: "image": "./assets/widgets/bg.png"

icons (for type "list" / "single")

Bundle icon images at build time. Referenced by name at runtime.

{
  "icons": {
    "brainstorm": "./assets/widgets/icons/brainstorm.png",
    "meditation": "./assets/widgets/icons/meditation.png",
    "focus": "./assets/widgets/icons/focus.png"
  }
}

At runtime, use the icon name:

{ icon: 'brainstorm', title: 'Morning Focus' }

On iOS, the icon is checked: if a bundled image matches the name, it's used as Image(name). Otherwise, it's treated as an SF Symbol (Image(systemName:)).

style

{
  "style": {
    "backgroundColor": "#1E1E2E",
    "titleColor": "#CDD6F4",
    "subtitleColor": "#A6ADC8",
    "accentColor": "#F38BA8"
  }
}

These provide widget-level defaults. Items can override per-item via WidgetItemText objects.

ios — Shared iOS config

| Property | Type | Default | Description | | -------------------- | --------- | ------------------ | ---------------------------------------- | | deploymentTarget | string | "17.0" | Minimum iOS version for widget extension | | useAppGroups | boolean | true | Enable App Groups for data sharing | | appGroupIdentifier | string | group.{bundleId} | Custom App Group ID | | devTeam | string | — | Apple development team ID |

Important: useAppGroups: true requires the App Group capability to be configured in your Apple Developer account. For local development without provisioning, set useAppGroups: false (widget won't receive data from the app).

android — Shared Android config

| Property | Type | Default | Description | | -------------------- | -------- | ------------------------ | --------------------------------- | | minSdkVersion | number | 26 | Minimum SDK for widget | | updatePeriodMillis | number | 1800000 | Auto-update interval (min 30 min) | | minWidth | number | 110 | Default widget width in dp | | minHeight | number | 40 | Default widget height in dp | | resizeMode | string | "horizontal\|vertical" | Resize behavior |


Runtime API

NNWidgets.updateWidget(widgetName, data)

Push structured data to a specific widget and reload its timeline.

import { NNWidgets } from "nn-widgets";

// List-type widget
await NNWidgets.updateWidget("SessionsList", {
  items: [
    {
      icon: "star.fill",
      title: "Morning Focus",
      description: "25 min",
      deepLink: "myapp://session/1",
    },
    {
      icon: { url: "meditation", size: 28, radius: 14 },
      title: { text: "Wind Down", color: "#CDD6F4", fontWeight: "bold" },
      description: { text: "15 min", color: "#A6ADC8", fontSize: "caption" },
    },
  ],
});

// Single-type widget
await NNWidgets.updateWidget("StatsWidget", {
  title: "Focus Time",
  subtitle: "This week",
  value: 142,
});

WidgetDataPayload

| Field | Type | Used by | Description | | ---------- | ------------------ | ----------------------- | --------------------- | | title | string | single, list (fallback) | Title text | | subtitle | string | single, list (fallback) | Subtitle text | | value | number | single | Numeric display value | | items | WidgetListItem[] | list | Array of list items |

WidgetListItem

| Field | Type | Default | Description | | ------------- | -------------------------- | ------------ | ------------------------------- | | icon | string \| WidgetItemIcon | — | Left icon (name or config) | | rightIcon | string \| WidgetItemIcon | — | Right icon (name or config) | | title | string \| WidgetItemText | required | Title text or styled text | | description | string \| WidgetItemText | — | Description text or styled text | | deepLink | string | — | Per-item deep link URL |

WidgetItemIcon (object form)

| Field | Type | Default | Description | | ----------------- | -------- | ------------ | ----------------------------------- | | url | string | required | SF Symbol name or bundled icon name | | size | number | 32 | Icon size in points/dp | | radius | number | 0 | Corner radius (size/2 = circle) | | backgroundColor | string | — | Background color (hex) |

Icon styling examples:

// Simple SF Symbol (default size 32, no background)
icon: "star.fill"

// Circular icon with background
icon: { url: "star.fill", size: 40, radius: 20, backgroundColor: "#FFD700" }

// Square icon with rounded corners
icon: { url: "brain", size: 36, radius: 8, backgroundColor: "#3B3B5C" }

// Right icon (e.g. chevron indicator)
rightIcon: { url: "chevron.right", size: 20, radius: 0 }

// Right icon as circular badge
rightIcon: { url: "checkmark.circle.fill", size: 24, radius: 12, backgroundColor: "#4CAF50" }

Both icon (left) and rightIcon (right) accept the same format — either a string or a WidgetItemIcon object.

WidgetItemText (object form)

| Field | Type | Default | Description | | ------------ | ------------------ | -------------------------------------------- | ---------------- | | text | string | required | Text content | | color | string | widget style | Text color (hex) | | fontSize | WidgetFontSize | "subheadline" (title) / "caption" (desc) | Font size key | | fontWeight | WidgetFontWeight | "semibold" (title) / "regular" (desc) | Font weight |

Font Size Reference

| Key | iOS (SwiftUI) | Android (sp) | | ------------- | ----------------------- | ------------ | | caption2 | .caption2 (11pt) | 11sp | | caption | .caption (12pt) | 12sp | | footnote | .footnote (13pt) | 13sp | | subheadline | .subheadline (15pt) | 15sp | | body | .body (17pt) | 17sp | | headline | .headline (17pt bold) | 17sp bold | | title3 | .title3 (20pt) | 20sp | | title2 | .title2 (22pt) | 22sp | | title | .title (28pt) | 28sp |

Font Weight Reference

| Key | iOS | Android | | ---------- | ----------- | ----------------------- | | regular | .regular | Typeface.NORMAL | | medium | .medium | Typeface.NORMAL + 500 | | semibold | .semibold | Typeface.BOLD | | bold | .bold | Typeface.BOLD |

Max Items per Widget Size (iOS)

| Size | Max Items | | ------------------ | --------- | | systemSmall | 3 | | systemMedium | 3 | | systemLarge | 7 | | systemExtraLarge | 7 |

Other API Methods

// Legacy flat key-value data (backward compatible)
await NNWidgets.setWidgetData({ key: "value", count: 42 });

// Get stored data
const data = await NNWidgets.getWidgetData();

// Reload a specific widget
await NNWidgets.reloadWidget({ widgetName: "MyWidget" });

// Reload all widgets
await NNWidgets.reloadWidget();

// Check support
const supported = NNWidgets.isSupported();

How Data Flows

┌──────────────────────┐
│   React Native App   │
│                      │
│  NNWidgets.update    │
│  Widget("List", {    │
│    items: [...]      │
│  })                  │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│   Native Module      │
│   (NNWidgetsModule)  │
│                      │
│  Stores key-value    │
│  pairs to shared     │
│  storage             │
│                      │
│  iOS: UserDefaults   │
│    (App Groups)      │
│  Android: Shared     │
│    Preferences       │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│   Widget Extension   │
│   (TimelineProvider) │
│                      │
│  Reads items JSON    │
│  from shared storage │
│  Parses & renders    │
│  the widget UI       │
└──────────────────────┘

Key Naming Convention

Data is stored with the widget_ prefix:

| JS call | Storage key | Widget reads | | ------------------------------------------ | --------------------- | --------------------------- | | updateWidget("MyList", { title: "Hi" }) | widget_MyList_title | getString("MyList_title") | | updateWidget("MyList", { items: [...] }) | widget_MyList_items | getJSON("MyList_items") | | setWidgetData({ foo: "bar" }) | widget_foo | getString("foo") |


Full Example: app.json

{
  "plugins": [
    [
      "nn-widgets",
      {
        "widgets": [
          {
            "name": "MainWidget",
            "displayName": "My App",
            "type": "single",
            "deepLinkUrl": "myapp://widget",
            "showAppIcon": true,
            "widgetFamilies": ["systemSmall"]
          },
          {
            "name": "RecentSessions",
            "displayName": "Recent Sessions",
            "type": "list",
            "deepLinkUrl": "myapp://sessions",
            "widgetFamilies": ["systemMedium", "systemLarge"],
            "fallbackTitle": "Recent Sessions",
            "fallbackSubtitle": "Open app to see sessions",
            "icons": {
              "focus": "./assets/widgets/icons/focus.png",
              "relax": "./assets/widgets/icons/relax.png"
            },
            "style": {
              "backgroundColor": "#1E1E2E",
              "titleColor": "#CDD6F4",
              "subtitleColor": "#A6ADC8",
              "accentColor": "#F38BA8"
            }
          },
          {
            "name": "BannerWidget",
            "displayName": "Banner",
            "deepLinkUrl": "myapp://banner",
            "widgetFamilies": ["systemMedium"],
            "image": {
              "medium": "./assets/widgets/banner.png"
            }
          },
          {
            "name": "QuickGrid",
            "displayName": "Quick Grid",
            "type": "grid",
            "gridLayout": "3x2",
            "deepLinkUrl": "myapp://grid",
            "widgetFamilies": ["systemMedium", "systemLarge"],
            "fallbackTitle": "Quick Grid",
            "fallbackSubtitle": "Open app to set up",
            "style": {
              "backgroundColor": "#1A1A2E",
              "titleColor": "#E0E0E0",
              "accentColor": "#BB86FC"
            }
          }
        ],
        "ios": {
          "devTeam": "YOUR_TEAM_ID",
          "useAppGroups": true,
          "appGroupIdentifier": "group.com.myapp"
        }
      }
    ]
  ]
}

Full Example: Pushing data from a screen

import React, { useEffect } from 'react';
import { NNWidgets } from 'nn-widgets';

function SessionsScreen({ sessions }) {
  useEffect(() => {
    // Update list widget with styled icons
    NNWidgets.updateWidget('RecentSessions', {
      items: sessions.slice(0, 7).map((s) => ({
        icon: { url: s.type, size: 32, radius: 8, backgroundColor: '#3B3B5C' },
        title: { text: s.name, fontWeight: 'semibold' },
        description: { text: `${s.duration} min`, fontSize: 'caption' },
        rightIcon: { url: 'chevron.right', size: 16 },
        deepLink: `myapp://session/${s.id}`,
      })),
    });
  }, [sessions]);

  return (/* ... */);
}

Full Example: Grid widget data

// Grid widget with 3x2 layout (3 columns, 2 rows, max 6 items)
await NNWidgets.updateWidget('QuickGrid', {
  items: [
    {
      icon: { url: 'star.fill', size: 40, radius: 20, backgroundColor: '#FFD700' },
      title: 'Favorites',
      deepLink: 'myapp://favorites',
    },
    {
      icon: { url: 'clock.fill', size: 40, radius: 20, backgroundColor: '#4A90D9' },
      title: 'Recent',
      deepLink: 'myapp://recent',
    },
    {
      icon: { url: 'bolt.fill', size: 40, radius: 20, backgroundColor: '#FF6B6B' },
      title: 'Quick Start',
      deepLink: 'myapp://quick',
    },
    {
      icon: { url: 'person.2.fill', size: 40, radius: 20, backgroundColor: '#66BB6A' },
      title: 'Team',
      deepLink: 'myapp://team',
    },
    {
      icon: { url: 'chart.bar.fill', size: 40, radius: 20, backgroundColor: '#AB47BC' },
      title: 'Stats',
      deepLink: 'myapp://stats',
    },
    {
      icon: { url: 'gearshape.fill', size: 40, radius: 20, backgroundColor: '#78909C' },
      title: 'Settings',
      deepLink: 'myapp://settings',
    },
  ],
});

Troubleshooting

Widget shows fallback text ("Open app to start")

  • The app hasn't called updateWidget() yet, or useAppGroups is false
  • Ensure useAppGroups: true in ios config and the App Group is provisioned

Widget doesn't update after calling updateWidget()

  • updateWidget() automatically calls reloadWidget() after storing data
  • iOS may delay widget updates. Force refresh by removing and re-adding the widget

Build error: "App Group not configured"

  • Add the App Group capability in Apple Developer portal
  • The App Group ID must match appGroupIdentifier (default: group.{bundleIdentifier})
  • For local dev without provisioning, set useAppGroups: false

Icons not showing

  • Bundled icons: ensure paths in icons config are correct and files exist
  • SF Symbols: use exact symbol names (e.g., "star.fill", not "star")
  • Android: icon names are lowercased for drawable resources

Requirements

  • Expo SDK 53+
  • iOS 17.0+ (for WidgetKit)
  • Android API 26+ (for App Widgets)

License

MIT