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

@proveanything/smartlinks-utils-ui

v2.0.4

Published

Reusable React components for SmartLinks microapps — Asset Picker, Conditions Editor, Icon Picker, and more.

Readme

@proveanything/smartlinks-utils-ui

Reusable React components for SmartLinks microapps — the headline module is RecordsAdminShell, a complete admin UI for the app.records pattern (global / rule-targeted / per-product data with automatic variant & batch drill-down where the collection supports them). Also ships an Asset Picker, Icon Picker, Font Picker, Link Picker, Conditions Editor, and a friendly Liquid template builder.

  • Package: @proveanything/smartlinks-utils-ui
  • Tracks: @proveanything/smartlinks ≥ 1.9
  • Per-component reference docs: docs/ — props, slots, behaviour, examples
  • Conceptual SDK docs: ui-utils.md and records-admin-pattern.md

Installation

npm install @proveanything/smartlinks-utils-ui

Peer dependencies

npm install react react-dom @proveanything/smartlinks @tanstack/react-query lucide-react

Supported host versions: React 18.3 or 19, @proveanything/smartlinks ^2.0.0, @tanstack/react-query ^5, lucide-react ^0.462 or ^1.x. React, the SDK, React Query, liquidjs, lucide-react and TipTap are all treated as externals — the package never bundles a second copy, so npm install needs no --legacy-peer-deps.

@tanstack/react-query is required by RecordsAdminShell (caching, pagination, optimistic save). Wrap your app in a <QueryClientProvider> somewhere up the tree.

Setup

Import the pre-compiled styles once in your app entry (e.g. main.tsx):

import '@proveanything/smartlinks-utils-ui/styles.css';

Components inherit your shadcn-compatible CSS variables (--primary, --background, --border, …) and pick up your theme automatically.

Tailwind CSS 4 hosts

V2 is built with Tailwind CSS 4. The package publishes fully compiled CSS, so your app should import styles.css as shown above but must not add this package to its Tailwind @source paths. Utilities inside the package resolve through your existing shadcn-compatible CSS variables, including .dark overrides; the package does not install a reset or replace your app's tokens.

When migrating a host app from Tailwind 3, follow Tailwind's normal v4 migration and keep these variables available: --background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --primary, --primary-foreground, --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --destructive, --destructive-foreground, --border, --input, --ring, and --radius.

V2 host checklist

  • Use React 18.3 or 19 and @proveanything/smartlinks 2.x.
  • Move the host app to Tailwind CSS 4 and retain its semantic CSS variables.
  • Import @proveanything/smartlinks-utils-ui/styles.css exactly once.
  • Keep QueryClientProvider above RecordsAdminShell.
  • Install TipTap peers only when importing /liquid-tiptap.
  • Remove picker-specific z-index, Escape, and scroll-lock workarounds; V2 handles nested dialogs through its shared layer manager.

Modules

| Subpath | Module | Use when | |---------|--------|----------| | /records-admin | RecordsAdminShell + hooks (useResolvedRecord, useRecordEditor, …) | Building any global / rule-targeted / per-product admin (variant & batch drill-down is automatic) | | /asset-picker | AssetPicker | Pick / upload / paste / URL-import media assets | | /icon-picker | IconPicker | Searchable Font Awesome 7 Pro picker | | /font-picker | FontPicker | Google Fonts + custom uploaded fonts | | /conditions-editor | ConditionsEditor | Recursive AND/OR rule builder (12 condition types) | | /facet-rule-editor | FacetRuleEditor | Server-side facet rules (AND-of-OR) for record targeting | | /link-picker | LinkPicker | External URL / app / deep-link picker (stores a LinkTarget) | | /liquid-editor | LiquidField + VariableBrowser, useLiquidFields, schema helpers | Let non-technical operators write Liquid templates by clicking labelled fields | | /liquid-tiptap | LiquidRichField + LiquidVariableNode | The same field chips inside a TipTap rich text editor (optional peer dep) |

Each module has a full reference doc under docs/:

Records Admin Shell

🎨 Admin chrome is neutral — not your brand. AdminPageHeader / RecordsAdminShell render deliberately neutral admin chrome (black/zinc/slate), and admin surfaces must support dark mode. Do not pull the public/customer theme into admin: no branded headers, no customer colours, no marketing fonts. The public surface is where the brand lives; admin is a consistent operator tool across every app. (App authors routinely reach for the public theme here — resist it.)

⚠️ Before you mount the shell, decide cardinality. If your app is a list of things (auction items, FAQs, image gallery, perks) you want items.cardinality: 'list'. If it's one winning record per scope (warranty, nutrition, care instructions) keep the default 'singleton'. The default is 'singleton', so multi-item apps must opt in — see Choosing cardinality. ('collection' is still accepted as a deprecated alias for 'list'.)

import {
  RecordsAdminShell,
  type EditorContext,
} from '@proveanything/smartlinks-utils-ui/records-admin';
import * as SL from '@proveanything/smartlinks';

<RecordsAdminShell<NutritionData>
  SL={SL}
  collectionId={collectionId}
  appId={appId}
  recordType="nutrition"
  label="Nutrition info"
  scopes={['collection', 'rule', 'product']}
  contextScope={{ productId, variantId, batchId }} // optional, from iframe URL
  defaultData={() => ({})}
  renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
  renderPreview={({ resolved }) => <pre>{JSON.stringify(resolved, null, 2)}</pre>}
/>

The shell renders the browser pane (scope tabs, search, status pills), the editor pane (sticky save / discard / delete footer with optimistic save), and the inheritance markers — you only provide the form for one record.

If the side list has only one configured scope, its scope selector is hidden automatically. Override with rail.scopeNavigation: 'show' | 'hidden', or rename visible choices with rail.scopeLabels (for example { collection: 'All posts' }).

For a blog, FAQ, gallery or other multi-entry publisher, use list cardinality and name the entries once. The built-in title, create buttons, search and empty state then use that terminology automatically. A global-only list opens as a full-page table/card browser; adding Rules or Products brings back the scope rail.

<RecordsAdminShell
  scopes={allowTargeting ? ['collection', 'rule', 'product'] : ['collection']}
  items={{
    cardinality: 'list',
    noun: 'blog post',
    pluralNoun: 'blog posts',
    views: ['table', 'cards'],
  }}
  {...props}
/>

When a noun is the first word of a built-in string (the list heading defaults to the plural noun on its own), it is capitalised automatically — "New blog post", "No blog posts yet", but the list heading reads "Blog posts". Templates that lead with other words keep the noun exactly as configured.

On the public widget side, read the resolved value with useResolvedRecord:

import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';

const { data, source, isLoading } = useResolvedRecord({
  SL, appId, recordType: 'nutrition',
  collectionId, productId, variantId, batchId,
});
// source: 'proof' | 'batch' | 'variant' | 'product' | 'rule' | 'collection' | null

Asset Picker

import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';

<AssetPicker
  scope={{ type: 'collection', collectionId: 'abc123' }}
  mode="dialog"
  allowUpload
  accept={['image/*']}
  onSelect={(asset) => setHeroUrl(asset.url)}
  trigger={<button>Choose image</button>}
/>

Conditions Editor

import { ConditionsEditor } from '@proveanything/smartlinks-utils-ui/conditions-editor';

<ConditionsEditor
  value={rules}
  onChange={setRules}
  collectionId={collectionId} // auto-loads facet definitions
  versions={[{ title: 'Default', value: '' }]}
  tags={['featured', 'new']}
/>

Icon Picker

import { IconPicker } from '@proveanything/smartlinks-utils-ui/icon-picker';

<IconPicker
  mode="dialog"
  value="fa-solid fa-heart"
  onSelect={(icon) => setIcon(icon.name)}
  trigger={<button>Pick icon</button>}
/>

Requires the Font Awesome 7 Pro kit script on the host page.

Font Picker

import { FontPicker } from '@proveanything/smartlinks-utils-ui/font-picker';

<FontPicker
  mode="dialog"
  value="Inter"
  showPreview
  onSelect={(font) => {
    console.log(font.cssFontFamily); // ready for `font-family:` CSS
    console.log(font.loadSnippet);   // <link> or @font-face block to inject
  }}
/>

Link Picker

Always persist the LinkTarget union — never a resolved URL — and resolve it at runtime with SL.navigation.resolveLink().

import { LinkPicker, type LinkTarget } from '@proveanything/smartlinks-utils-ui/link-picker';

<LinkPicker
  collectionId={collectionId}
  value={link}                    // LinkTarget | undefined
  onChange={setLink}
/>

Supports external URLs, other apps in the collection, and deep links into an app (including custom deep-link params). See link-picker.md.

Liquid Templates

A Liquid template builder aimed at non-technical operators: they click labelled fields ("Product name", "Lot number") and never have to type Liquid. Chips render inline, clicking one opens a settings panel (swap field, fallback, date style, number padding, capitalisation), and a live preview + friendly validation sit underneath. What gets saved is ordinary Liquid.

import { LiquidField, useLiquidFields } from '@proveanything/smartlinks-utils-ui/liquid-editor';

// Optional: ask the platform which custom fields this brand actually has
const { schema, sampleData } = useLiquidFields({
  collectionId,
  productId, proofId, batchId, variantId, lotId, // optional — pulls real example values
  objects: ['contact', 'product', 'proof', 'lot'],
  extraSchema: [{
    key: 'order',
    label: 'Order',
    variables: [{ path: 'order.reference', label: 'Order reference', example: 'ORD-1042' }],
  }],
});

<LiquidField
  label="Email body"
  value={body}
  onChange={setBody}
  schema={schema}
  sampleData={sampleData}
/>

Key points for host apps:

  • Objects you support — the editor does not guess the brand's subscription. Pass objects={['contact','product','lot']} to hide the rest (collection, product, proof (item record), variant, batch, lot, contact, attestation, interaction, case, thread, record, user). A bare key keeps the whole group; a dotted key offers a single field, e.g. objects={['contact.firstName','contact.lastName','product']}.
  • Standard SDK objects — interactions, cases, conversations and app records are built in with their standard wrapper fields. Their free-form payloads (data, owner, admin, metadata) are app-specific: pass a real record via useLiquidFields({ samples: { case, thread, record, interaction } }) to have those keys discovered with live examples, or declare them in extraSchema.
  • Your own fields — merge them with extraSchema; they appear as a normal group in the field list with their example values.
  • Custom fields, automatically — useLiquidFields reads the brand's declared product / item / contact field definitions from the SDK and, when given record IDs, real example values. Failures are ignored per-source, so a brand without lots just gets fewer groups.
  • Single-line use — <LiquidField singleLine showPreview={false} /> for subject lines and short labels.
  • liquidjs is bundled as a dependency and lazy-loaded — apps that only use VariableBrowser never download the engine.

Rich text variant (Markdown out, chips in the editor):

import { LiquidRichField } from '@proveanything/smartlinks-utils-ui/liquid-tiptap';

<LiquidRichField value={markdown} onChange={setMarkdown} format="markdown" />

TipTap is an optional peer dependency — install it only if you use this component: npm i @tiptap/react @tiptap/core @tiptap/pm @tiptap/starter-kit tiptap-markdown.

The toolbar is yours to arrange — pass an ordered list of built-in buttons, custom buttons, and a 'spacer' that pushes the rest to the right. Handy when the message is mostly hand-written text and the dynamic fields are a side option:

<LiquidRichField
  value={body}
  onChange={setBody}
  toolbar={['bold', 'italic', 'separator', 'bulletList', 'spacer', 'fields']}
/>

Extra TipTap extensions go in via extraExtensions, paired with your own toolbar button.

Full reference: liquid-editor.md · liquid-tiptap.md.

Dialogs and nested modals

Every modal surface in the toolkit (Asset Picker, Icon Picker, Font Picker, Link Picker, the Records Admin dialogs and the Liquid field panels) runs through one shared layer stack. That means you can open any picker from inside your own dialog — including a Radix Dialog — without host-side workarounds:

  • page scroll is locked once and restored exactly once, by the last layer to close;
  • each layer stacks above its parent automatically (no z-index juggling);
  • Escape closes only the topmost layer and does not bubble to your dialog;
  • focus moves into the new layer on open and returns to the previous element on close;
  • clicks inside our panels never reach a host click-outside handler.

If you previously suppressed Escape or unlocked scrolling by hand around a picker, you can remove that code.

Prerequisites

All components assume @proveanything/smartlinks is initialised in your app via SL.initializeApi(). Admin components (RecordsAdminShell, AssetPicker upload, etc.) call the SDK with admin: true — do not render them in public-facing views.

Tree shaking

Each component has its own subpath export — bundle only what you use:

// Bundles only RecordsAdmin
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';

// Barrel import — bundler tree-shakes the rest
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui';

If you use subpath imports, import styles.css separately — subpaths do not pull it in automatically.

Development

cd packages/smartlinks-ui
npm install
npm run build    # tsup
npm run dev      # watch mode

License

MIT