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/portal-framework

v0.1.3

Published

Configurable portal frame and layout components for SmartLinks micro-apps

Downloads

513

Readme

@proveanything/portal-framework

A configurable portal frame and content orchestration system for building engagement platforms on top of the SmartLinks ecosystem.

Overview

Portal Framework provides the building blocks for creating mobile-first portal experiences that navigate SmartLinks entities (Collections → Products → Proofs). It handles:

  • Layout Configuration — Headers, side menus, tabs (top/bottom), and special features
  • Content Orchestration — Manages navigation through the entity hierarchy
  • Micro-app Integration — Embeds external apps via iframes with context passing
  • Two-Layer Configuration — Platform-level constraints + collection-level overrides
  • Theming — Full CSS variable-based theming with dark mode support

Installation

npm install @proveanything/portal-framework

Quick Start

import { 
  PortalFrame, 
  ContentOrchestrator,
  NavigationProvider,
  usePortalConfig,
  resolveConfig,
} from '@proveanything/portal-framework';

const App = () => {
  const { config } = usePortalConfig({
    fetchConfig: async () => {
      // Fetch collection-level overrides from SmartLinks
      return await SL.appConfiguration.getConfig({ collectionId, appId });
    },
  });

  // Merge template constraints with collection overrides
  const finalConfig = resolveConfig(myPlatformTemplate, config);

  return (
    <NavigationProvider collectionId={collectionId}>
      <PortalFrame config={finalConfig}>
        <ContentOrchestrator 
          collectionId={collectionId}
          availableApps={myApps}
        />
      </PortalFrame>
    </NavigationProvider>
  );
};

Architecture: Two-Layer Configuration

Portal apps use a two-layer configuration system that separates platform constraints (what's possible) from collection customization (what a brand chooses).

Layer 1: Platform Template (Static)

Defined in your app's codebase as a PlatformTemplate. This is hardcoded — editing it requires a code change. It controls:

  • Which apps are available for menus/tabs
  • Which features are enabled (QR scanner, language selector, account)
  • Locked items that collections cannot remove
  • Maximum item counts and customization limits
// src/config/template.ts — lives in your app, not in the API
import type { PlatformTemplate } from '@proveanything/portal-framework';

export const myTemplate: PlatformTemplate = {
  id: 'fan-connect',
  name: 'Fan Connect',

  availableApps: [
    { id: 'home', name: 'Home', icon: 'Home', category: 'Core' },
    { id: 'fixtures', name: 'Fixtures', icon: 'Calendar', category: 'Sports' },
    { id: 'points', name: 'Points', icon: 'Award', category: 'Engagement' },
    // ... only apps THIS platform supports
  ],

  availableFeatures: {
    qrScanner: true,
    languageSelector: true,
    account: true,
    pointsCollection: true,
  },

  // Locked items cannot be removed by collection admins
  requiredBottomTabs: [
    { id: 'points-tab', label: 'Points', icon: 'Award', appId: 'points',
      visible: true, order: 4, locked: true },
  ],

  constraints: {
    allowCustomLinks: true,
    allowDividers: true,
    maxBottomTabs: 5,
    maxSideMenuItems: 15,
    allowHeaderCustomization: true,
    allowSideMenuPositionChange: true,
  },

  // Starting config for new collections
  defaultConfig: {
    bottomTabs: {
      enabled: true,
      showLabels: true,
      items: [
        { id: 'home-tab', label: 'Home', icon: 'Home', appId: 'home', visible: true, order: 0 },
        { id: 'fixtures-tab', label: 'Fixtures', icon: 'Calendar', appId: 'fixtures', visible: true, order: 1 },
      ],
    },
  },
};

Layer 2: Collection Config (Dynamic)

Stored via SL.appConfiguration.setConfig() and loaded at runtime. Each collection (brand) can customize their portal within the bounds set by the template:

// Loaded from SmartLinks API at runtime
const collectionOverrides = await SL.appConfiguration.getConfig({
  collectionId,
  appId: 'portal-config',
});

// e.g.:
// {
//   header: { title: 'Arsenal FC', logoUrl: 'https://...' },
//   bottomTabs: {
//     items: [
//       { id: 'home-tab', ... },
//       { id: 'shop-tab', label: 'Shop', icon: 'ShoppingCart', appId: 'shop', ... },
//     ]
//   }
// }

Merging with resolveConfig()

The resolveConfig utility merges both layers while enforcing all constraints:

import { resolveConfig } from '@proveanything/portal-framework';

const finalConfig = resolveConfig(myTemplate, collectionOverrides);

What resolveConfig enforces:

| Constraint | Behavior | |-----------|----------| | locked: true items | Always present, cannot be removed | | maxBottomTabs / maxSideMenuItems / maxTopTabs | Excess unlocked items are trimmed | | allowHeaderCustomization: false | Collection header overrides are ignored | | allowSideMenuPositionChange: false | Side menu position stays at template default | | allowCustomLinks: false | Custom-link menu items are filtered out | | allowDividers: false | Divider menu items are filtered out | | availableFeatures | Disabled features are forced off regardless of overrides | | availableApps | Menu/tab items referencing unavailable apps are filtered out |

Microsite Mode (Default Portal)

For SMBs that just want a custom domain with their own portal, use a permissive template and resolve collectionId from the domain:

// Default Portal app — most permissive template
const micrositeTemplate: PlatformTemplate = {
  id: 'microsite',
  name: 'Custom Portal',
  availableApps: [ /* all apps */ ],
  availableFeatures: { qrScanner: true, languageSelector: true, account: true },
  constraints: {
    allowCustomLinks: true,
    allowDividers: true,
    maxSideMenuItems: 20,
    maxBottomTabs: 5,
    allowHeaderCustomization: true,
    allowSideMenuPositionChange: true,
  },
};

// Resolve collectionId from domain (future SDK support)
const collectionId = await resolveCollectionFromDomain(window.location.hostname);
const overrides = await SL.appConfiguration.getConfig({ collectionId, appId: 'portal-config' });
const config = resolveConfig(micrositeTemplate, overrides);

Components

Layout Components

| Component | Purpose | |-----------|---------| | <PortalFrame> | Main container with header, menus, and tabs | | <PortalHeader> | Header with logo, back button, and action buttons | | <PortalSideMenu> | Slide-out menu (left or right) | | <PortalTabs> | Tab navigation (top or bottom) | | <DynamicIcon> | Renders Lucide icons by name |

Orchestration Components

| Component | Purpose | |-----------|---------| | <ContentOrchestrator> | Manages which content to show based on navigation state | | <NavigationProvider> | Context provider for navigation state | | <AppIframeRenderer> | Renders micro-apps in iframes with caching | | <OrchestratedPortal> | All-in-one: PortalFrame + ContentOrchestrator + NavigationProvider |

Default Views

| Component | Purpose | |-----------|---------| | <DefaultProductList> | Grid of products in a collection | | <DefaultProofView> | Single proof display with attestations | | <DefaultAppTabs> | Tab interface for multiple apps on a proof | | <OrchestratorBreadcrumbs> | Navigation breadcrumbs | | <AnimatedContent> | Fade transitions between views |


Customization Slots

The ContentOrchestrator provides slot-based customization at each hierarchy level. Higher levels support more customization; lower levels are increasingly standardized.

Available Slots

| Slot | Level | Props Interface | Use Case | |------|-------|-----------------|----------| | collectionHome | Collection | CollectionSlotProps | Full override of the collection landing page | | collectionHeader | Collection | CollectionSlotProps | Custom header above the product list | | productList | Collection | ProductListSlotProps | Custom product grid/list layout | | productHome | Product | ProductSlotProps | Full override of the product detail page | | productHeader | Product | ProductSlotProps | Custom header above product app tabs | | proofHeader | Proof | ProofSlotProps | Custom header above proof details |

Slot Props

// Collection-level props
interface CollectionSlotProps {
  collection: Collection;
  apps: CollectionApp[];
  onNavigateToProduct: (productId: string) => void;
  onNavigateToApp: (appId: string) => void;
}

// Product list props
interface ProductListSlotProps {
  collection: Collection;
  apps: CollectionApp[];
  onNavigateToProduct: (productId: string) => void;
  onNavigateBack: () => void;
}

// Product-level props
interface ProductSlotProps {
  collection: Collection;
  product: Product;
  apps: CollectionApp[];
  onNavigateToProof: (proofId: string) => void;
  onNavigateToApp: (appId: string) => void;
  onNavigateBack: () => void;
}

// Proof-level props
interface ProofSlotProps {
  collection: Collection;
  product: Product;
  proof: Proof;
  apps: CollectionApp[];
  attestations: Attestation[];
  onNavigateToApp: (appId: string) => void;
  onNavigateBack: () => void;
}

Customization Gradient

Collection level  ████████████████  High customization
Product level     ██████████        Medium customization
Proof level       ████              Low customization (standardized)

Example: Custom Collection Landing Page

<ContentOrchestrator
  collectionId={collectionId}
  slots={{
    collectionHome: ({ collection, apps, onNavigateToProduct }) => (
      <div className="p-4">
        <h1 className="text-2xl font-bold">{collection.name}</h1>
        <p>{collection.description}</p>
        {/* Your custom product grid */}
        <CustomProductGrid onSelect={onNavigateToProduct} />
      </div>
    ),
  }}
  defaults={{
    appLayout: 'tabs',       // 'tabs' | 'accordion' | 'stack'
    proofApps: ['warranty', 'info'],  // Filter which apps show at proof level
  }}
/>

Embedding Micro-Apps

The AppIframeRenderer component embeds SmartLinks micro-apps inside iframes with full lifecycle management.

Basic Usage

import { AppIframeRenderer } from '@proveanything/portal-framework';

<AppIframeRenderer
  appId="warranty-registration"
  collectionId="abc123"
  productId="prod456"
  proofId="proof789"
  onReady={() => console.log('App loaded')}
  onRouteChange={(path, state) => console.log('Route:', path)}
/>

Props

| Prop | Type | Description | |------|------|-------------| | appId | string | The micro-app identifier | | collectionId | string | Current collection context | | productId? | string | Current product context | | proofId? | string | Current proof context | | isAdmin? | boolean | Whether to load admin interface | | cache? | IframeCacheData | Pre-loaded data to avoid loading delays | | version? | 'stable' \| 'development' | Which app version to load | | appUrl? | string | Override URL for local development | | initialPath? | string | Deep link path within the app | | onRouteChange? | (path, state) => void | Called when iframe route changes | | onResize? | (height) => void | Called when iframe height changes | | onReady? | () => void | Called when app is loaded and ready | | onError? | (error) => void | Called on load failure | | minHeight? | number | Minimum iframe height (default: 200) |

Context Passing

The renderer passes context to embedded apps via URL parameters:

https://app.example.com/#/?collectionId=abc&productId=prod456&proofId=proof789&appId=warranty

Apps receive these parameters and use them to load relevant data via the SmartLinks SDK.

Pre-loading with Cache

Pass pre-fetched data to avoid loading flickers:

<AppIframeRenderer
  appId="warranty"
  collectionId={collectionId}
  cache={{
    collection: collectionData,
    product: productData,
    proof: proofData,
    user: { uid: 'user123', email: '[email protected]' },
  }}
/>

PostMessage Communication

The AppIframeRenderer uses the SmartLinks IframeResponder which handles:

| Event | Direction | Description | |-------|-----------|-------------| | smartlinks-route-change | App → Parent | App navigated to a new route | | smartlinks-resize | App → Parent | App content height changed | | smartlinks-ready | App → Parent | App finished loading | | smartlinks-auth-login | App → Parent | User logged in within the app | | smartlinks-auth-logout | App → Parent | User logged out within the app | | Cache updates | Parent → App | Updated entity data pushed to app |


CollectionApp Metadata

Apps discovered via SL.collection.getAppsConfig(collectionId) include rich metadata:

interface CollectionApp {
  id: string;
  srcAppId?: string;            // Original app template ID
  name: string;
  publicIframeUrl?: string;     // URL for embedding
  active?: boolean;
  icon?: string;                // Lucide icon name
  faIcon?: string;              // Font Awesome icon identifier
  description?: string;
  category?: 'Authenticity' | 'Documentation' | 'Commerce' | 'Engagement'
    | 'AI' | 'Digital Product Passports' | 'Integration' | 'Web3' | 'Other';

  // Visibility & access control
  ownersOnly?: boolean;         // Only show to proof owners
  hidden?: boolean;             // Hidden from UI (background apps)

  // Widget & embedding support
  manifestUrl?: string;         // URL to app's manifest for widget definitions
  supportsDeepLinks?: boolean;  // App supports multi-page navigation

  // Usage contexts — which levels this app appears at
  usage?: {
    collection?: boolean;
    product?: boolean;
    proof?: boolean;
    widget?: boolean;
  };
}

Filtering by Visibility

// Only show apps the current user should see
const visibleApps = apps.filter(app => {
  if (app.hidden) return false;
  if (app.ownersOnly && !isOwner) return false;
  return true;
});

Configuration Types

PortalLayoutConfig

interface PortalLayoutConfig {
  header: HeaderConfig;
  sideMenu: SideMenuConfig;
  topTabs: TopTabsConfig;
  bottomTabs: BottomTabsConfig;
  specialFeatures: SpecialFeaturesConfig;
}

See the type definitions for full interface details.

PlatformTemplate

interface PlatformTemplate {
  id: string;
  name: string;
  description?: string;
  logoUrl?: string;
  primaryColor?: string;

  availableApps: AppDefinition[];
  availableFeatures: {
    qrScanner?: boolean;
    languageSelector?: boolean;
    account?: boolean;
    quickActions?: boolean;
    pointsCollection?: boolean;
    [key: string]: boolean | undefined;
  };

  requiredSideMenuItems?: SideMenuItem[];
  requiredBottomTabs?: TabItem[];
  requiredTopTabs?: TabItem[];

  constraints: {
    allowCustomLinks: boolean;
    allowDividers: boolean;
    maxSideMenuItems?: number;
    maxBottomTabs?: number;
    maxTopTabs?: number;
    allowHeaderCustomization: boolean;
    allowSideMenuPositionChange: boolean;
  };

  defaultConfig?: Partial<PortalLayoutConfig>;
}

Hooks

usePortalConfig

Manages portal configuration state with helpers for updates:

const {
  config,
  isLoading,
  error,
  updateHeader,
  updateSideMenu,
  addSideMenuItem,
  updateSideMenuItem,
  removeSideMenuItem,
  reorderSideMenuItems,
  updateTopTabs,
  updateBottomTabs,
  addTabItem,
  removeTabItem,
  updateSpecialFeatures,
  resetConfig,
} = usePortalConfig({
  initialConfig,  // Optional starting config
  fetchConfig,    // Async config fetcher
  onChange,        // Change callback
});

useNavigation

Access and control navigation state:

const {
  state,          // NavigationState
  actions,        // NavigationActions
  getBreadcrumbs, // () => Breadcrumb[]
} = useNavigation();

// State includes: currentLevel, selectedProduct, selectedProof, availableApps, etc.
// Actions include: navigateToProduct, navigateToProof, navigateToApp, navigateBack

useNavigationState

Subscribe to navigation state with fetch integration:

const { state, products, proofs, isLoading } = useNavigationState({
  collectionId,
  fetchProducts: async (collectionId) => { ... },
  fetchProofs: async (collectionId, productId) => { ... },
});

Navigation Levels

| Level | What's Displayed | Default Component | |-------|------------------|-------------------| | collection | List of products | <DefaultProductList> | | product | Product detail + app tabs | <DefaultAppTabs> | | proof | Proof details + app tabs | <DefaultProofView> + <DefaultAppTabs> |


Styling

The framework uses Tailwind CSS with CSS custom properties. Override variables in your app:

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
}

Example Configurations

The package includes example configurations for reference:

import { 
  fanConnectSportsTemplate,  // Sports club engagement
  fanConnectMusicTemplate,   // Band/artist engagement
  smartDocentTemplate,       // Museum/gallery experience
  genericTemplate,           // Starter configuration
} from '@proveanything/portal-framework';

// Use as reference, then build your own:
const finalConfig = resolveConfig(fanConnectSportsTemplate, collectionOverrides);

These are examples, not presets. Use them as reference when building your own configurations.


Building a Portal App

For production use, create a separate application that imports this framework:

// my-portal-app/src/App.tsx
import { 
  OrchestratedPortal,
  NavigationProvider,
  resolveConfig,
} from '@proveanything/portal-framework';
import { myTemplate } from './config/template';

export const App = () => {
  const collectionId = getCollectionIdFromContext();
  const overrides = useCollectionConfig(collectionId); // fetch from API
  const config = resolveConfig(myTemplate, overrides);
  
  return (
    <NavigationProvider collectionId={collectionId}>
      <OrchestratedPortal
        collectionId={collectionId}
        config={config}
        showBreadcrumbs={true}
        enableAnimations={true}
      />
    </NavigationProvider>
  );
};

Site Injection System

The framework supports a Site Injection system that lets parent platforms inject custom React components without modifying the framework:

  • Actions: Components triggered from tabs, menus, or programmatically — presented as sheets, modals, or full pages
  • Zones: Passive components rendered at contentHeader, contentFooter, headerRight, and heroOverlay
const injections: SiteInjections = {
  actions: {
    'ai-chat': {
      id: 'ai-chat',
      component: AiChatPanel,
      presentation: 'sheet',
      showAt: ['product', 'proof'],
    },
  },
  zones: {
    contentHeader: StatusBanner,
  },
};

<OrchestratedPortal
  collectionId={collectionId}
  config={config}
  siteInjections={injections}
/>

See docs/site-injection.md for full documentation.


Documentation

Detailed guides are available in the docs/ directory:

| Guide | Description | |-------|-------------| | Architecture | System layers, component roles, configuration flow | | Site Injection | Actions, zones, badges, level-aware visibility | | Direct Component Rendering | Same-origin widget loading, bare import shimming | | Cross-App Communication | postMessage protocols, shared contexts, capability contracts | | Authentication | AuthKit integration, auth modal, custom auth | | Getting Started | Lovable initialization prompt for new portal apps |


License

MIT