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

udi-yac

v0.1.6

Published

React implementation of the Universal Discovery Interface (UDI) chat — natural-language querying and visualization of biomedical datasets. Publishable as a library (`UDIChat` component) or as a standalone SPA.

Downloads

140

Readme

udi-yac

React implementation of the UDI Chat interface — an AI-powered system for querying and visualizing biomedical datasets via natural language. This is a React port of the original Vue 3/Quasar udi-chat app. Published on npm as udi-yac; the repository directory remains udi-chat-react.

Quick Start

pnpm install
pnpm dev          # dev server
pnpm build        # standalone app build (dist/)
pnpm build:lib    # library build (dist/, consumes entry from src/index.ts)
pnpm lint         # eslint
pnpm format       # prettier
pnpm typecheck    # tsc --noEmit
pnpm test         # vitest (one-shot)
pnpm test:watch   # vitest in watch mode

Standalone app config (env vars)

The standalone App.tsx reads these Vite env vars (see .env.example). Copy .env.example to .env.local to override locally:

| Var | Default | Purpose | | -------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | VITE_UDI_API_BASE_URL | http://localhost:8007 | UDIAgent FastAPI server URL | | VITE_UDI_DATA_PACKAGE | (unset → inline HuBMAP API package from src/data/hubmapRemote.ts) | Optional path/URL to a datapackage_udi.json. Overrides the inline default when set. | | VITE_UDI_REQUIRE_API_KEY | true | Set to false to skip the in-app OpenAI key prompt | | VITE_UDI_MODEL | (unset) | Optional LLM model override |

By default the standalone app talks to the live HuBMAP Portal metadata API. To use the locally bundled snapshot instead, set:

VITE_UDI_DATA_PACKAGE=/data/hubmap_2025-05-05/datapackage_udi.json

Note on build vs build:lib: pnpm build produces a deployable standalone SPA — this is the default so CI/deploy pipelines behave as expected. To build the publishable library bundle (the UDIChat React component), use pnpm build:lib, which invokes vite build --mode lib and emits both JS and .d.ts files under dist/.

Stack

  • React 19 with TypeScript
  • Tailwind 4 + shadcn/ui (Base UI primitives)
  • Zustand for state management (vanilla stores via React Context)
  • UDI Toolkit (Vue Custom Elements) for grammar-based visualization rendering
  • Arquero for client-side data loading and domain computation
  • Vite for dev server and library builds

Architecture

Dual Build Modes

The project builds as both a library and a standalone app:

  • Library (pnpm build): Exports the UDIChat component and UDIChatConfig type. Consumers provide React and render <UDIChat> with configuration props.
  • Standalone (pnpm build:app): Builds App.tsx as a full SPA with dev defaults.

Library Usage

import { UDIChat } from 'udi-yac';
import 'udi-yac/style.css';

<UDIChat
  apiBaseUrl="http://localhost:8007"
  dataPackagePath="./data/hubmap_2025-05-05/datapackage_udi.json"
  authToken="your-jwt-token" // optional
  requireApiKey // optional — prompts for OpenAI key
  model="agenticx/UDI-VIS-Beta-v2" // optional
/>;

Config Props

| Prop | Type | Description | | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------- | | apiBaseUrl | string | Base URL for the UDIAgent API | | dataPackagePath | string? | URL/path to datapackage_udi.json. Ignored when dataPackage is provided. | | dataPackage | DataPackage? | Provide a data package object directly instead of fetching from a URL. Takes precedence over dataPackagePath. | | dataFieldDomains | DataFieldDomain[]? | Pre-computed field domains. Skips CSV loading for domain computation when provided with dataPackage. | | fetchOptions | RequestInit? | Custom fetch options (headers, credentials, etc.) forwarded to all data-loading fetch calls. | | authToken | string? | JWT bearer token for API auth | | requireApiKey | boolean? | Show API key input before chatting | | model | string? | LLM model name override | | downloadActions | DownloadAction[]? | Extra items appended to the Download Data dropdown. See Custom download actions. | | entityIcons | EntityIconMap? | Icon overrides for entity count chips. See Custom entity icons. | | mascot | ReactNode \| null? | Replace or hide the welcome mascot. See Custom mascot. | | splashMessages | readonly string[]? | Override or hide the randomised prompt above the mascot. See Custom splash messages. | | onEvent | TrackerFn? | Analytics callback invoked on key user actions. See Analytics events. | | className | string? | CSS class for the root element | | style | CSSProperties? | Inline styles for the root element |

Data Source Configuration

There are three ways to provide data to UDIChat, from simplest to most flexible:

1. Local data package file (default)

Point dataPackagePath to a datapackage_udi.json file. The JSON must contain a udi:path base path and resources with relative file paths. CSVs are loaded client-side via Arquero.

<UDIChat
  apiBaseUrl="http://localhost:8007"
  dataPackagePath="./data/hubmap_2025-05-05/datapackage_udi.json"
/>

2. Remote data sources

Data packages can reference remote URLs. Set udi:path to a remote base URL and keep resource path values as relative filenames. Arquero's loadCSV uses fetch() internally, so remote URLs work out of the box.

If the remote server requires authentication, pass fetchOptions with the necessary headers. These are forwarded to all fetch() calls — both the data package JSON fetch and CSV loading:

<UDIChat
  apiBaseUrl="http://localhost:8007"
  dataPackagePath="https://portal.example.com/metadata/datapackage_udi.json"
  fetchOptions={{
    headers: { Authorization: 'Bearer <token>' },
    credentials: 'include',
  }}
/>

Note: The remote server must send appropriate CORS headers (Access-Control-Allow-Origin) for browser-based fetching to work.

3. Inline data package (no fetch)

Pass a DataPackage object directly via the dataPackage prop. This is useful when you build the schema programmatically or receive it from an API. CSVs are still loaded from the URLs in udi:path + resource.path for domain computation, unless you also provide dataFieldDomains to skip that step entirely.

import type { DataPackage } from 'udi-yac';

const myDataPackage: DataPackage = {
  'udi:path': 'https://portal.hubmapconsortium.org/metadata/v0/',
  resources: [
    {
      name: 'donors',
      path: 'donors.tsv',
      'udi:row_count': 281,
      schema: {
        fields: [
          {
            name: 'age_value',
            description: 'The time elapsed since birth.',
            'udi:data_type': 'quantitative',
          },
          { name: 'sex', description: 'Biological sex of the donor.', 'udi:data_type': 'nominal' },
          // ... more fields
        ],
      },
    },
    // ... more resources
  ],
};

<UDIChat apiBaseUrl="http://localhost:8007" dataPackage={myDataPackage} />;

To skip CSV loading entirely (e.g. when you already have domain metadata), pass pre-computed domains:

import type { DataFieldDomain } from 'udi-yac';

const myDomains: DataFieldDomain[] = [
  {
    entity: 'donors',
    field: 'age_value',
    type: 'interval',
    fieldDescription: 'The time elapsed since birth.',
    domain: { min: 1, max: 87 },
  },
  {
    entity: 'donors',
    field: 'sex',
    type: 'point',
    fieldDescription: 'Biological sex of the donor.',
    domain: { values: ['Male', 'Female'] },
  },
  // ...
];

<UDIChat
  apiBaseUrl="http://localhost:8007"
  dataPackage={myDataPackage}
  dataFieldDomains={myDomains}
/>;

See src/data/hubmapRemote.ts for the canonical inline DataPackage example targeting the live HuBMAP Portal — this is also the default the standalone App.tsx uses.

Features

Chat Interface

  • Natural language input with LLM-powered responses
  • Tool call rendering: visualizations, filters, explanations, clarifications, rebuffs
  • Example prompts dialog (fetched from backend /v1/yac/examples)
  • Conversation reset, save/export as JSON
  • API key input with localStorage persistence

Visualization Dashboard

  • Auto-pinned visualizations from assistant responses
  • Interactive Vega-Lite charts via UDI Grammar spec → UDIVis (Vue CE)
  • Cross-chart filtering: brush selections on one chart filter all others
  • Expand/collapse visualizations to full width
  • Table view toggle: switch between chart and raw data table
  • Field tweaking: swap x/y/color encodings via dropdowns
  • Spec inspector: view/copy JSON spec, open in UDI Grammar Editor (lz-string compressed URL)
  • Hover highlighting across chat messages and dashboard cards
  • Memory bank: restore recently closed visualizations

Data Filtering

  • Interval filters: range sliders for numeric fields
  • Point filters: checkbox selection for categorical fields
  • Filter toolbar: active filter chips with clear buttons
  • Cross-entity filtering: filters propagate across related entities via foreign keys
  • Null value filtering toggle

Data Management

  • Entity counts: per-entity row counts with dynamic filtered counts
  • Download: filtered data as ZIP of CSVs, or manifest (hubmap_id extraction). Consumers can extend the dropdown with custom actions via the downloadActions prop — see Custom download actions.
  • Data package loading with domain computation (Arquero)

Custom download actions

Pass downloadActions on UDIChatConfig to append consumer-specific entries to the Download Data dropdown. Each action's onClick receives a snapshot of the current filters and the per-source rows the built-in "Download Raw Data" would have used, so you can export to custom formats, post to an API, or route to another tool.

import { UDIChat } from 'udi-yac';
import type { DownloadAction } from 'udi-yac';

const sendToWorkspaces: DownloadAction = {
  label: 'Open in Workspaces',
  disabled: (ctx) => ctx.rowsBySource.every((r) => r.rows.length === 0),
  onClick: async ({ rowsBySource, filters }) => {
    const ids = rowsBySource.flatMap(({ rows }) =>
      rows.map((r) => String(r['hubmap_id'] ?? '')).filter(Boolean),
    );
    await fetch('/api/workspaces', {
      method: 'POST',
      body: JSON.stringify({ ids, filters }),
    });
  },
};

<UDIChat apiBaseUrl="http://localhost:8007" downloadActions={[sendToWorkspaces]} />;

The DownloadActionContext passed to each callback contains:

| Field | Type | Notes | | -------------- | ----------------------------------- | ------------------------------------------------------------------ | | rowsBySource | { source: string; rows: Row[] }[] | Post-filter, post-brush rows — same data the built-in ZIP exports. | | filters | DataSelections | Active filter selections keyed by filter id. | | dataPackage | DataPackage \| null | The loaded data package; null until first resolution completes. |

Custom actions render after the two built-in items, separated by a divider.

Custom entity icons

Pass entityIcons on UDIChatConfig to change the icon rendered on each entity count chip in the dashboard header. Keys are entity names exactly as they appear in the data package (resources[].name). Any component that accepts a className prop works — lucide-react icons are typical.

import { UDIChat } from 'udi-yac';
import type { EntityIconMap } from 'udi-yac';
import { Dna, FlaskConical } from 'lucide-react';

const icons: EntityIconMap = {
  // Override the default icon for an existing entity:
  samples: FlaskConical,
  // Add an icon for a custom entity:
  sequencing_runs: Dna,
};

<UDIChat apiBaseUrl="http://localhost:8007" entityIcons={icons} />;

Consumer entries are merged on top of the built-in icons (donors, samples, datasets, …) — you only need to supply the names you want to override or add. Entities with no match fall back to a generic table icon.

Custom mascot

The empty-dashboard welcome splash renders a YAC mascot by default. Consumers can replace it or hide it via the mascot prop on UDIChatConfig:

| Value | Result | | ------------------ | ---------------------------------------------------------------------- | | undefined (omit) | Renders the built-in YAC mascot image. | | null | Hides the mascot entirely. The speech-bubble prompt above still shows. | | Any ReactNode | Renders the provided node in place of the mascot image. |

import { UDIChat } from 'udi-yac';

// Replace with a custom image:
<UDIChat
  apiBaseUrl="http://localhost:8007"
  mascot={<img src="/my-mascot.svg" alt="" className="w-60 h-60 object-contain" />}
/>

// Hide entirely:
<UDIChat apiBaseUrl="http://localhost:8007" mascot={null} />

Custom splash messages

One prompt is picked at random from a built-in pool ("Ask me for a visualization!", "What data would you like to explore?", etc.) and shown in the speech bubble above the mascot. Override via splashMessages on UDIChatConfig:

| Value | Result | | ------------------ | ------------------------------------------------------ | | undefined (omit) | Random pick from the built-in defaults. | | Non-empty array | Random pick from the provided strings exclusively. | | [] | Hides the speech bubble entirely (mascot still shows). |

<UDIChat
  apiBaseUrl="http://localhost:8007"
  splashMessages={[
    'Ask me about donors, samples, or datasets.',
    'Try “average age by sex”.',
  ]}
/>

// Hide the speech bubble:
<UDIChat apiBaseUrl="http://localhost:8007" splashMessages={[]} />

The selection is made once per UDIChat mount, so the message doesn't flicker between renders.

Analytics events

Pass an onEvent callback on UDIChatConfig to receive events for key user actions. The signature —

type TrackerFn = (name: string, properties?: Record<string, unknown>) => void;

— matches the call shape of every major analytics tool, so it can usually be forwarded directly:

import { UDIChat } from 'udi-yac';
import type { TrackerFn } from 'udi-yac';

// Google Analytics 4 (via gtag):
const track: TrackerFn = (name, props) => window.gtag?.('event', name, props);

// Segment / Amplitude / PostHog:
// const track: TrackerFn = (name, props) => window.analytics?.track(name, props);

<UDIChat apiBaseUrl="http://localhost:8007" onEvent={track} />;

Event names are stable, snake_case strings. Properties carry metadata only — never raw message content or OpenAI key material. If the callback throws, the error is swallowed so an analytics failure never breaks the chat.

Correlation ids. Every event carries a sessionId (minted once per UDIChat mount) so all events from one chat instance can be stitched together and two tabs can be told apart. A subset of events — the ones bound to a single send↔response round trip — also carries a shared turnId, making it trivial to match a message_sent to the response_received, rebuff_received, or request_failed it produced. A retry after entering an API key reuses the original turnId so the retry's response still pairs back to the originating send.

| Event | Fired when | Properties | | -------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | message_sent | User submits a message through the chat input | turnId: stringcharCount: number — length of the text, not the text itselfconversationLength: number — messages already in the conversation when this one was addedhasUserApiKey: boolean — whether the request will carry the user's key | | response_received | Server completes a /v1/yac/completions call (including rebuffs) | turnId: string — matches the originating message_sentdurationMs: numbertoolCallNames: string[] — e.g. ["RenderVisualization", "FreeTextExplain"]toolCallCount: numberhadUserKey: booleanhasRebuff: boolean | | rebuff_received | The response contains a Rebuff tool_call | turnId: stringreason?: string — machine-readable discriminator; "budget_exceeded" for quota rebuffs, absent for ordinary rebuffshadUserKey: boolean | | request_failed | /v1/yac/completions throws (network error, HTTP !ok) | turnId: stringdurationMs: numberhadUserKey: boolean | | api_key_set | User submits a key through the ApiKeyInput | inResponseToQuota: boolean — true if a budget-exceeded rebuff had triggered the prompt (distinguishes first-time entry from quota-recovery entry) | | api_key_cleared | User clears their stored key via the header icon | (none) | | conversation_reset | User clicks the Reset button (clears messages, pinned viz, filters, memory bank) | conversationLength: number — message count at the moment of reset | | visualization_pinned | A new visualization is auto-pinned from an assistant response | hasTitle: booleantoolCallIndex: number — position within the assistant message's tool_calls | | visualization_closed | User clicks the × on a pinned visualization card | hasTitle: boolean | | download_raw_data | User clicks "Download Raw Data" in the Download Data dropdown | sources: number — count of sources contributing rowsrowsTotal: number | | download_manifest | User clicks "Download Manifest" | idsTotal: number — count of hubmap_id values in the exported manifest | | download_<slug> | User clicks a custom downloadActions entry | label: string — the original menu label | | filter_range_changed | User commits a new range on a quantitative (interval) filter slider, including the reset button | entity: stringfield: stringisReset: boolean — true when triggered by the reset buttonisFullRange: boolean — true when the new range covers the full domain | | filter_selection_changed | User toggles a checkbox or clicks "Clear all" on a categorical (point) filter | entity: stringfield: stringaction: 'toggle' \| 'clear_all'checked?: boolean — present only when action === 'toggle'selectionCount: number — count of selected values after the change (no values themselves) | | filter_entity_changed | User changes the target entity on a tweakable filter card | filterType: 'interval' \| 'point'entity: string — the newly selected entityfield: string — the field carried over to the new entity (may be empty) | | filter_field_changed | User changes the target field on a tweakable filter card | filterType: 'interval' \| 'point'entity: stringfield: string — the newly selected field | | visualization_tweaked | User swaps the field bound to an encoding via a VizTweakComponent dropdown | encoding: string — the encoding channel (e.g. "x", "color") |

Every row in the table above also includes sessionId: string; it's omitted from the per-event cells to keep the table scannable.

Custom download slug. The suffix is derived from the action's label: a leading "Download " is stripped, the rest is lowercased and non-alphanumeric runs are replaced with _. So a { label: "Download All TSVs" } action emits download_all_tsvs; { label: "Open in Workspaces" } emits download_open_in_workspaces. The original label is always echoed back in properties.label so consumers can distinguish collisions.

Properties you will not see. By design, the following never cross the tracker boundary: raw message text, tool_call arguments, OpenAI API keys, data rows, filter values. Only counts, booleans, tool-call names, ids, and short slug strings are emitted.

Debug Mode (type !/admin in chat)

  • System prompts toggle (show/hide system messages)
  • Conversation sidebar drawer (load saved session JSON files)
  • Export test case for benchmarking
  • Download data domains / data schema as JSON
  • Save conversation export

Project Structure

The codebase follows a bulletproof-react-style layout. Module boundaries are enforced by eslint-plugin-project-structure (see eslint.config.js). For the reasoning behind the layout and a guide to working within it, see CONTRIBUTING.md.

src/
  index.ts                          # Library entry: exports UDIChat + UDIChatConfig
  index.css                         # Global Tailwind base + custom CSS
  env.d.ts                          # Vite client types

  app/                              # Composition root (allowed to reach into any feature)
    main.tsx                        # Vite app bootstrap
    App.tsx                         # Standalone app entry (inline HuBMAP package)
    UDIChat.tsx                     # Root component (provider + layout)
    UDIChatConfig.ts                # UDIChatConfig type (extracted to break circular)
    UDIChatContext.tsx              # Provider + hooks wiring all Zustand stores
    ErrorBoundary.tsx               # React error boundary
    validateConfig.ts (+ .test.ts)  # Runtime validation for UDIChatConfig

  features/
    chat/
      index.ts                      # Public barrel — cross-feature consumers import only from here
      api/
        completions.ts              # POST /v1/yac/completions client + QueryConfig type
      components/
        ChatPanel.tsx               # Slim orchestrator (~85 lines)
        ChatHeaderBar.tsx           # Toolbar — owns debugMode subscription
        DebugToggleSection.tsx      # System-prompt toggle — owns debugMode + messages
        ClosedVisualizationsPanel.tsx  # Recently-closed viz strip — owns memoryBank
        ChatInput.tsx               # Message input
        MessageList.tsx             # Message history with auto-scroll
        MessageBubble.tsx           # Single message + tool call tabs
        ConversationList.tsx        # Sidebar with saved session files
        ApiKeyInput.tsx             # OpenAI API key input
      hooks/
        useChatApi.ts               # LLM API integration hook
        useExamplePrompts.ts        # /v1/yac/examples fetch
        useResetHandlers.ts         # Bundled "reset everything" action
        useDebugExports.ts          # Debug-mode export buttons (save / test case / data)
      stores/
        conversationStore.ts        # Chat messages, save/load/export

    dashboard/
      index.ts                      # Public barrel
      components/
        DashboardPanel.tsx          # Dashboard layout (counts, filters, viz grid)
        DashboardCard.tsx           # Single pinned viz (chart, toolbar, tweak, spec)
        DataCounts.tsx              # Per-entity row counts (total + filtered)
        FilterToolbar.tsx           # Active filter chips
        DownloadButton.tsx          # CSV/manifest download dropdown
        VizTweakComponent.tsx       # Field encoding swap dropdowns
        VizTweakComponent.types.ts  # TweakableParam, LayerLike, MappingLike
        WelcomeSplash.tsx           # Empty dashboard placeholder
      stores/
        dashboardStore.ts           # Pinned vizzes, interactivity, expand/table/hover
        dataFiltersStore.ts         # Interval/point filter state, message sync
        selectionsStore.ts          # Cross-viz brush selection coordination
        memoryBankStore.ts          # Closed visualization restoration

    data-package/
      index.ts                      # Public barrel
      types.ts                      # Web Worker protocol types
      stores/
        dataPackageStore.ts         # Data schema, field domains, entity relationships
      utils/
        joinDataPath.ts             # Path joining for local + remote data URLs
        structuredTextParser.ts     # Template function evaluation for explanations
      workers/
        domainWorker.ts             # Off-main-thread domain computation

    tool-calls/
      index.ts                      # Public barrel
      types.ts                      # Args for each tool call type
      components/
        ToolCallRenderer.tsx        # Dispatches tool calls to renderers
        VisualizationCard.tsx       # UDIVis preview (chat) / pinned badge
        FilterComponent.tsx         # Filter dispatcher (interval/point)
        IntervalFilterComponent.tsx
        PointFilterComponent.tsx
        FreeTextExplain.tsx         # Markdown explanation with structured text
        RebuffNotice.tsx            # Rejection with suggestion buttons
        ClarifyVariable.tsx         # Field disambiguation UI

  components/
    ui/                             # shadcn/ui primitives (badge, button, dialog, …)

  stores/
    globalStore.ts                  # Truly cross-feature state (debug mode)

  types/
    messages.ts                     # Message, ToolCall, FlatToolCall (cross-feature)
    dataPackage.ts                  # DataPackage, DataFieldDomain, etc. (cross-feature)

  lib/
    utils.ts                        # cn() helper (clsx + tailwind-merge)

  utils/
    specMutations.ts                # Pure UDI grammar helpers

  data/
    hubmapRemote.ts                 # Inline DataPackage targeting the live HuBMAP Portal API

Module boundaries

The project-structure/independent-modules rule enforces these import boundaries:

| From | Can import | | -------------------- | ------------------------------------------------------------------------------------------------ | | src/features/X/** | own family, other features' index.ts only, src/{utils,types,lib,stores,components/ui}/** | | src/app/** | any feature internal, all shared layers | | src/components/ui/ | sibling UI, src/lib/ | | src/utils/ | src/{utils,types,lib,stores}/, feature barrels | | src/{types,lib}/ | shared layers only | | src/stores/ | src/{stores,types,lib}/ |

Cross-feature imports must go through the feature's index.ts barrel — direct paths like @/features/dashboard/stores/dataFiltersStore from another feature will fail lint.

API Integration

The app communicates with a UDIAgent backend:

| Endpoint | Method | Purpose | | ---------------------- | ------ | --------------------------------- | | /v1/yac/completions | POST | Send messages, receive tool calls | | /v1/yac/examples | GET | Fetch example prompts | | /sessions/{filename} | GET | Load saved conversation files |

Request body for completions:

{
  "model": "...",
  "messages": [...],
  "dataSchema": "...",
  "dataDomains": "..."
}

Relationship to udi-grammar

Visualizations are rendered by the UDIVis Vue Custom Element from the udi-grammar package, consumed via the published udi-toolkit npm package. The React wrapper (udi-toolkit/react) bridges Vue CE props and events:

  • Props (spec, selections): set via useLayoutEffect on the DOM element
  • Events (selection-change, data-ready): listened via addEventListener, with Vue CE array-wrapping unwrapped