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.11

Published

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

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;
}

TagsBlockRenderer

Renders value as a list of tags. A single string is rendered as a one-item list; an empty array renders an em-dash placeholder.

interface TagsBlock extends Block {
    type: 'tags';
    value: string | string[];
}

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

Tabs filter which form rows are visible. A row is shown on a tab when its tabId matches the active tab; rows without a tabId are visible on every tab. The tab bar is only rendered when there is more than one tab.

| Field | Type | Required | Description | |---|---|---|--------------------------------------------------------------| | id | string | yes | Unique tab identifier; referenced from RowDefinition.tabId | | label | string | no | Tab label; omit to use the autokey screens.{screenId}.tabs.{tabId} |

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

Each action wraps a child block (typically a button) that renders the action's UI. The wrapping ScreenAction renderer owns the execution state and confirmation flow; the child block triggers it by calling execute(fn) from the ActionContext.

| Field | Type | Required | Description | |---|---|---|---| | id | string | yes | Unique action identifier | | label | string | no | Action label exposed via ActionContext; omit to use the autokey screens.{screenId}.actions.{actionId} | | activate | "always" \| "onDirty" \| "onValid" \| "onDirtyAndValid" | yes | When the action should be enabled | | confirmation | ConfirmationDefinition | yes | Confirmation dialog settings | | block | Block | yes | Child block rendered inside the action; it reads ActionContext to render its UI and invoke execute |

Please note that the block contains a value property. In the context of actions, this is not used but it might prove useful in the future, for selecting an additional value.

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 |

ActionContext

The action's child block is rendered inside an ActionContext provider. A custom block can consume it via the useActionContext hook to render its UI and trigger the action:

| Field | Type | Description | |---|---|---| | id | string | The action's id | | label | string | Resolved label (translated, or derived from the autokey) | | isEnabled | boolean | false while the action is executing | | isExecuting | boolean | true between execute being called and the returned promise settling | | execute | (fn: () => Promise<void>) => void | Runs fn; if confirmation.askConfirmation === 'always', the confirmation dialog is shown first and fn runs on confirm |

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

function SaveButton() {
    const { label, isEnabled, execute } = useActionContext();
    return (
        <button disabled={!isEnabled} onClick={() => execute(async () => { /* call API */ })}>
            {label}
        </button>
    );
}

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) | | visibleWhen | VisibleWhen | When present, the row is rendered only if the predicate matches — see Conditional visibility | | tabId | string \| string[] | Restricts the row to the given tab(s); rows without a tabId are visible on every tab. Only meaningful when the screen defines tabs |

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

ColumnDefinition

| Field | Type | Description | |---|---|---| | elements | ElementDefinition[] | Child elements rendered in this column |

Conditional visibility

RowDefinition accepts an optional visibleWhen predicate. The bound value is resolved using the standard binding syntax and compared against one of the matchers below. When the predicate evaluates to false the row is omitted entirely from the rendered output.

VisibleWhen

| Field | Type | Description | |---|---|---| | binding | string | Binding expression resolved against the screen data (e.g. $data#$.contentType) | | equals | unknown | Match when the resolved value is strictly equal to the given value | | startsWith | string | Match when the resolved value is a string that starts with the given prefix | | oneOf | unknown[] | Match when the resolved value is one of the values in the list | | matches | string | Match when the resolved value is a string matching the given regular expression | | exists | boolean | When true, match when the resolved value is defined (not undefined/null); when false, match when absent |

Example — show a group of fields only for image content types:

{
  "displayType": "group",
  "groupId": "metadata-image",
  "visibleWhen": {
    "binding": "$data#$.contentType",
    "startsWith": "image/"
  },
  "elements": [
    { "type": "label", "value": "$data#$.image.width" },
    { "type": "label", "value": "$data#$.image.height" }
  ]
}

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. A binding is a source prefix followed by # and a JSONPath expression rooted at $:

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

The portion after # is a standard JSONPath, evaluated with jsonpath-plus. Anything valid in JSONPath is supported, including:

  • Dot or bracket child access — $.user.city, $['user']['city']
  • Array index access — $.items[0].title
  • Bracket notation for keys with special characters — $['Caption/Abstract'], $.metadata['dc:title']
  • Wildcards — $.items[*].title
  • Recursive descent — $..title
  • Filter expressions — $.items[?(@.published==true)].title

Resolution semantics:

  • Single match → the matched value is returned directly (string, number, object, etc.).
  • Multiple matches (wildcards, recursive descent, filters) → an array of matched values is returned.
  • No matchundefined.

The value field on an ElementDefinition supports three forms:

Single string — resolves to a single value (or array, for multi-match expressions) 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 | | 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 derived from the binding's JSONPath — e.g. $data#$.title produces title, and $data#$.address.city produces address.city. Bracket notation is normalized into segments ($.items[0].titleitems.0.title), and characters that would conflict with the dot-separated key format — colons (:) and whitespace — are replaced with _, so a binding such as $data#$.metadata['dc:title'] produces the autokey segment metadata.dc_title.

For elements rendered inside an array element's itemTemplate, an $itemData#$.field binding's autokey is prefixed with the enclosing array binding's path. For example, an array bound to $data#$.aanvullendeTitels with an item template field bound to $itemData#$.titel produces the autokey screens.{screenId}.{groupId}.aanvullendeTitels.titel. Nested arrays compose: each ItemDataProvider layer adds its array's path to the prefix.

An itemTemplate can also be expressed as { rows: RowDefinition[] } to lay out fields across multiple rows within each item — each row contains an elements array (and optionally nested rows or columns), reusing the same shape as the screen-level RowDefinition. Within a row, elements flow side by side; rows are stacked vertically inside the array item. This is useful for information-dense items or when a nested array within an item should occupy its own row.

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: either a map of field name → ElementDefinition (single-row layout), or { rows: RowDefinition[] } to group fields across multiple rows |

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.