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

@knaw-huc/panoptes-react-blocks

v0.0.4

Published

A React component library providing block renderers and a screen layout system for [Panoptes](https://github.com/knaw-huc/panoptes-react) applications.

Downloads

276

Readme

@knaw-huc/panoptes-react-blocks (WIP)

A React component library providing block renderers and a screen layout system for Panoptes applications.

Installation

npm install @knaw-huc/panoptes-react-blocks

Import the styles in your app entry point:

import '@knaw-huc/panoptes-react-blocks/style.css';

Peer dependencies

| Package | Version | |---|---| | react | 19 | | react-dom | 19 | | @knaw-huc/panoptes-react | * | | @tanstack/react-router | * |

Block renderers

Each renderer accepts a block prop typed as Block from @knaw-huc/panoptes-react. The block's type, value, and optional config fields drive the rendering.

ExternalLinkBlockRenderer

Renders a value string as an external anchor (target="_blank").

interface ExternalLinkBlock extends Block {
    type: 'external-link';
    value: string; // URL
}

LabelBlockRenderer

Renders a plain text value.

interface LabelBlock extends Block {
    type: 'label';
    value: string;
}

LinkBlockRenderer

Renders an internal navigation link using @tanstack/react-router. The URL is built from config.url and optional route model params.

interface LinkBlock extends Block {
    type: 'link';
    value: string;           // link text
    config?: { url: string }; // router path
    model?: Record<string, unknown>; // route params
}

MarkdownBlockRenderer

Renders value as Markdown (GFM + raw HTML). HTML is sanitized via rehype-sanitize.

interface MarkDownBlock extends Block {
    type: 'markdown';
    value: string;
}

ToggleBlockRenderer

Renders a boolean value as a check or cross icon (using Heroicons). Supports i18n via the Panoptes translateFn.

interface ToggleBlock extends Block {
    type: 'toggle';
    value: boolean;
}

MapBlockRenderer

Renders an OpenLayers map centred on a lat/lon coordinate with a marker. Defaults to OpenStreetMap tiles.

interface MapBlock extends Block {
    type: 'map';
    value: { latitude: number; longitude: number };
    config?: {
        zoom?: number;    // default 12
        tileUrl?: string; // custom OSM-compatible tile URL
    };
}

RenderJsonBlock

Renders structured JSON data driven by a JSON Schema config.

interface JsonBlock extends Block {
    type: 'json';
    value: JsonData;   // any JSON value
    config: JsonSchema; // JSON Schema object
}

ScreenBlockRenderer

Renders a full data-entry screen defined by a ScreenDefinition config. See the Screen system section below.

interface ScreenBlock extends Block {
    type: 'screen';
    value: ScreenBlockValue; // Record<string, unknown>
    config: ScreenDefinition;
}

Screen system

The screen block system (lib/components/blocks/screen/) renders structured detail screens driven by a declarative JSON configuration from the Panoptes API. A screen block is registered as a Panoptes block of type "screen" and can be used anywhere the framework renders blocks.

Architecture overview

RenderScreenBlock          # Registered Panoptes block component
└── ScreenProvider         # React context (screenDefinition + data + active tab)
    └── ScreenRenderer     # Top-level layout shell
        ├── ScreenLinks    # Optional navigation links (header area)
        ├── ScreenTabs     # Optional tab bar (hidden when only one tab)
        ├── ScreenSidebar  # Optional icon sidebar (Lucide icons)
        ├── ScreenForm     # Form body
        │   └── FormRow    # Recursive row rendering (header / group / footer / row)
        │       └── FormColumn → FormElement   # Column + element rendering
        └── ScreenActions  # Optional action buttons with confirmation dialogs

ScreenBlock config schema

A ScreenBlock received from the API has the following shape:

{
  "type": "screen",
  "value": { /* flat or nested data object */ },
  "config": { /* ScreenDefinition — see below */ }
}

ScreenDefinition

| Field | Type | Required | Description | |---|---|----------|--------------------------------------------------------------------------------------------------------------| | id | string | yes | Unique identifier | | label | string | no | Screen heading passed through translateFn; omit to use the autokey screens.{id} | | screenType | "normal" | yes | Screen layout variant (not used yet - intended for screen variants, eg., mobile, in a popover, confirmation) | | tabs | TabDefinition[] | no | Tab list; a tab bar is shown only when there are more than one | | activeTabId | string | no | Initially active tab (defaults to first tab) | | links | LinkDefinition[] | no | Navigation links rendered above the tabs | | actions | ActionDefinition[] | no | Action buttons rendered in the footer | | form | FormDefinition | yes | Content form (rows of elements) | | sidebar | SidebarDefinition | no | Icon sidebar rendered to the left of the form |

TabDefinition (not used yet)

Tabs are .... tabs, and meant for executing an operation after clicking a tab.

| Field | Type | Required | Description | |---|---|---|--------------------------------------------------------------| | id | string | yes | Unique tab identifier | | label | string | no | Tab label; omit to use the autokey screens.{screenId}.tabs.{tabId} | | operation | OperationDefinition | no | API operation to call when the tab is selected | | operationList | OperationListItem[] | no | Sub-navigation items shown beneath the active tab |

Each OperationListItem has an id, an optional label (autokey: screens.{screenId}.tabs.{tabId}.{itemId}), and an operation.

LinkDefinition (not used yet)

Links are intended to be 'follow-up' operations after fetching data. For example, we could envision the flow as follows:

  • Fetch item data
  • Link -> Fetch screen definition
  • Link -> Fetch translations
  • Link -> Fetch data from remote system

| Field | Type | Required | Description | |---|---|---|---| | id | string | yes | Unique link identifier | | label | string | yes | Link label | | operation | OperationDefinition | no | API operation to execute on click | | href | string | no | URL to navigate to on click |

ActionDefinition (not used yet)

Actions are meant to be simple API operations, executed after action button clicks.

| Field | Type | Required | Description | |---|---|---|---| | id | string | yes | Unique action identifier | | label | string | no | Button label; omit to use the autokey screens.{screenId}.actions.{actionId} | | activate | "always" \| "onDirty" \| "onValid" \| "onDirtyAndValid" | yes | When the button is enabled | | confirmation | ConfirmationDefinition | yes | Confirmation dialog settings | | operation | OperationDefinition | yes | API operation to call on confirm |

ConfirmationDefinition

| Field | Type | Description | |---|---|---| | askConfirmation | "always" \| "never" \| "onDirty" | When to show a confirmation dialog | | labels.title | string | Dialog title; omit to use the autokey screens.{screenId}.actions.{actionId}.title | | labels.message | string | Dialog message; omit to use the autokey screens.{screenId}.actions.{actionId}.message | | labels.ok | string | Confirm button label; omit to use the autokey screens.{screenId}.actions.{actionId}.ok | | labels.cancel | string | Cancel button label; omit to use the autokey screens.{screenId}.actions.{actionId}.cancel |

FormDefinition and rows

{
  "form": {
    "rows": [ /* RowDefinition[] */ ]
  }
}

RowDefinition

| Field | Type | Description | |---|---|---| | displayType | "header" \| "group" \| "footer" \| "row" | Styling variant (default: "row") | | label | string | Optional fieldset legend; omit to use the autokey screens.{screenId}.{groupId} (requires groupId) | | groupId | string | Used as the React key and data-group-id attribute | | elements | ElementDefinition[] | Direct child elements (mutually exclusive with columns/rows) | | columns | ColumnDefinition[] | Multi-column layout; each column holds its own elements | | rows | RowDefinition[] | Nested rows (recursive) |

Row content is resolved in order of priority: nested rowscolumnselements.

ElementDefinition

| Field | Type | Description | |---|---|---| | value | string \| string[] \| Record<string, string> | Binding expression(s) — see Bindings | | type | string | Element type (see Element types); inferred from data when omitted | | label | string | Field label rendered above the element; omit to use the autokey screens.{screenId}.{groupId}.{field} (only available for single-string value) | | infoLabel | string | Secondary info text rendered below the element; omit to use the autokey screens.{screenId}.{groupId}.{field}.info (only available for single-string value) | | hidden | boolean | Hides the element when true | | config | object | Type-specific configuration (e.g. options for select, itemTemplate for array) |

SidebarDefinition

| Field | Type | Description | |---|---|---| | id | string | Unique sidebar identifier | | width | string | Optional CSS width for the sidebar (sets --sidebar-width) | | sections | SidebarSectionDefinition[] | Groups of navigation items separated by a divider |

Each SidebarNavItemDefinition has an icon (Lucide icon name in kebab-case, e.g. "book-open"), an optional label (autokey: screens.{screenId}.sidebar.{sectionId}.{itemId}), an operation, and an optional active flag.

Bindings

Element value fields and itemTemplate field values use binding expressions to pull data from the block's payload:

| Expression | Source | |---|---| | $data#/field/subfield | The value object of the ScreenBlock | | $itemData#/field | The current item object when rendering inside an array element |

Path segments are separated by /.

The value field on an ElementDefinition supports three forms:

Single string — resolves to a single value and enables autokey label generation:

{ "value": "$data#/title", "type": "text" }

Array of strings — each binding is resolved independently; the block receives an array of resolved values. Useful for blocks that combine multiple data fields:

{ "value": ["$data#/firstName", "$data#/lastName"], "type": "full-name" }

Object with string values — each property's binding expression is resolved independently; the block receives a plain object with the resolved values. Useful for blocks that need named inputs (e.g. a map block needing separate latitude/longitude fields). No autokey is generated in this form:

{
  "value": {
    "latitude": "$data#/plaatsBreedtegraad",
    "longitude": "$data#/plaatsLengtegraad"
  },
  "type": "map",
  "config": { "zoom": 6 }
}

Autokey label generation

All label (and infoLabel) fields are optional. When omitted, a translation key is derived automatically and passed through translateFn. The keys follow a hierarchical pattern based on the screen ID, group ID, and field path:

| Context | Autokey pattern | Example | |---|---|---| | Screen heading | screens.{screenId} | screens.journal-detail | | Group legend | screens.{screenId}.{groupId} | screens.journal-detail.metadata | | Element label | screens.{screenId}.{groupId}.{field} | screens.journal-detail.metadata.title | | Element info label | screens.{screenId}.{groupId}.{field}.info | screens.journal-detail.metadata.title.info | | Tab label | screens.{screenId}.tabs.{tabId} | screens.journal-detail.tabs.general | | Tab operation list item | screens.{screenId}.tabs.{tabId}.{itemId} | screens.journal-detail.tabs.general.volume-1 | | Sidebar nav item | screens.{screenId}.sidebar.{sectionId}.{itemId} | screens.journal-detail.sidebar.main.home | | Action button | screens.{screenId}.actions.{actionId} | screens.journal-detail.actions.save | | Action confirmation title | screens.{screenId}.actions.{actionId}.title | screens.journal-detail.actions.save.title | | Action confirmation message | screens.{screenId}.actions.{actionId}.message | screens.journal-detail.actions.save.message | | Action confirmation ok | screens.{screenId}.actions.{actionId}.ok | screens.journal-detail.actions.save.ok | | Action confirmation cancel | screens.{screenId}.actions.{actionId}.cancel | screens.journal-detail.actions.save.cancel |

The {field} segment is the path from the binding expression — e.g. $data#/title produces title, and $data#/address/city produces address.city.

When a label is provided explicitly it is used as-is (also passed through translateFn), which allows overriding the autokey with a custom translation key or a literal string.

Element types

The element type can be an unspecified type, or a type present in the collection of Panoptes-known blocks (list, cmdi) and/or application-specific custom blocks.

When type is not specified on an ElementDefinition the type is inferred from the resolved value:

| Inferred condition | Type | |---|---| | Array | array | | Boolean | checkbox | | Number | number | | String matching YYYY-MM-DD… | date | | String containing \n | textarea | | Anything else | text |

Explicit types available:

| Type | Rendered as | Config options | |---|---|---| | text | <input type="text"> (read-only) | — | | textarea | <textarea> (read-only) | — | | number | <input type="number"> (read-only) | — | | date | <input type="date"> (read-only) | — | | checkbox | <input type="checkbox"> (read-only) | — | | prose | Inline <span> | — | | select | Resolved option label in <input type="text"> | config.options: { value, label }[] | | array | List of text inputs, or templated item rows | config.itemTemplate: map of field name → ElementDefinition |

Any type that matches a registered Panoptes block is rendered using that block component. If no matching block is found (or the block component throws), the element falls back to the native HTML renderer above.

Example of a full screen definition

{
  "id": "tijdschrift-detail",
  "screenType": "normal",
  "globals": {
  },
  "tabs": [
  ],
  "links": [
  ],
  "actions": [
  ],
  "sidebar": {
    "id": "tijdschrift-sidebar",
    "sections": [
      {
        "id": "main",
        "items": [
          {
            "id": "tijdschriften",
            "icon": "newspaper",
            "label": "tijdschrift-detail.tijdschrift-sidebar.label.publications"
          }
        ]
      },
      {
        "id": "util",
        "items": [
          {
            "id": "instellingen",
            "icon": "settings",
            "label": "tijdschrift-detail.tijdschrift-sidebar.label.settings"
          }
        ]
      }
    ]
  },
  "form": {
    "rows": [
      {
        "displayType": "group",
        "groupId": "titel",
        "columns": [
          {
            "elements": [
              {
                "value": "$data#/lidwoordTitel",
                "type": "label"
              }
            ]
          },
          {
            "elements": [
              {
                "value": "$data#/titelVanTijdschrift",
                "type": "label"
              }
            ]
          },
          {
            "elements": [
              {
                "value": "$data#/onderTitel",
                "type": "label"
              }
            ]
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "publicatie",
        "rows": [
          {
            "columns": [
              {
                "elements": [
                  {
                    "value": "$data#/uitgever",
                    "type": "link",
                    "config": {
                      "url": "/politieke-tijdschriften-uitgever_drukker/details/$uitgeverId"
                    }
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/drukker",
                    "type": "link",
                    "config": {
                      "url": "/politieke-tijdschriften-uitgever_drukker/details/$drukkerId"
                    }
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/plaats",
                    "type": "link",
                    "config": {
                      "url": "/politieke-tijdschriften-plaatsnaam/details/$plaatsId"
                    }
                  }
                ]
              }
            ]
          },
          {
            "columns": [
              {
                "elements": [
                  {
                    "value": "$data#/uitgeverZeker",
                    "type": "toggle"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/drukkerZeker",
                    "type": "toggle"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/nietBewaard",
                    "type": "toggle"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/vrijheidGelijkheidBroederschap",
                    "type": "toggle"
                  }
                ]
              }
            ]
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "periode",
        "rows": [
          {
            "columns": [
              {
                "elements": [
                  {
                    "value": "$data#/eersteNummer",
                    "type": "label"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/laatsteNummer",
                    "type": "label"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/prijsDuiten",
                    "type": "label"
                  }
                ]
              },
              {
                "elements": [
                  {
                    "value": "$data#/afleveringen",
                    "type": "label"
                  }
                ]
              }
            ]
          },
          {
            "elements": [
              {
                "value": "$data#/formaat",
                "type": "label"
              }
            ]
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "classificatie",
        "columns": [
          {
            "elements": [
              {
                "value": "$data#/vormTijdschrift",
                "type": "label"
              }
            ]
          },
          {
            "elements": [
              {
                "value": "$data#/typeTijdschrift",
                "type": "label"
              }
            ]
          },
          {
            "elements": [
              {
                "value": "$data#/politiekePositie",
                "type": "label"
              }
            ]
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "inhoud",
        "elements": [
          {
            "value": "$data#/korteOmschrijvingInhoud",
            "type": "markdown",
            "config": {
            }
          },
          {
            "value": "$data#/verantwoordingSelectie",
            "type": "markdown",
            "config": {
            }
          },
          {
            "value": "$data#/toelichtingRedacteurAuteur",
            "type": "markdown",
            "config": {
            }
          },
          {
            "value": "$data#/advertenties_en_andere_verwijsplaatsen",
            "type": "markdown"
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "aanvullende-titels",
        "elements": [
          {
            "value": "$data#/aanvullendeTitels",
            "type": "list"
          }
        ]
      },
      {
        "displayType": "group",
        "groupId": "artikel-types",
        "elements": [
          {
            "value": "$data#/artikelType",
            "type": "list"
          }
        ]
      }
    ]
  }
}

Utility components

GhostLine

A decorative horizontal line placeholder used for loading states.

import { GhostLine } from '@knaw-huc/panoptes-react-blocks';

<GhostLine />

Repository structure

| Path | Purpose | |---|---| | lib/ | Library source — compiled and published as @knaw-huc/panoptes-react-blocks | | src/ | Example show app for local development and visual testing |

Run the example app with npm run dev to preview components in the browser.

Building

npm run build   # compile once
npm run dev     # watch mode (starts the example app)

Output is written to dist/ as ES module (index.js) and CommonJS (index.cjs) bundles, with a bundled style.css.