npm package: @proveanything/portal-framework
Description: Configurable portal frame and layout components for SmartLinks micro-apps
Author: ProveAnything
Latest version: 0.1.4

# @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

```bash
npm install @proveanything/portal-framework
```

## Quick Start

```tsx
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

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
// 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

```typescript
// 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

```tsx
<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

```tsx
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:

```tsx
<AppIframeRenderer
  appId="warranty"
  collectionId={collectionId}
  cache={{
    collection: collectionData,
    product: productData,
    proof: proofData,
    user: { uid: 'user123', email: 'user@example.com' },
  }}
/>
```

### 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:

```typescript
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

```typescript
// 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

```typescript
interface PortalLayoutConfig {
  header: HeaderConfig;
  sideMenu: SideMenuConfig;
  topTabs: TopTabsConfig;
  bottomTabs: BottomTabsConfig;
  specialFeatures: SpecialFeaturesConfig;
}
```

See the [type definitions](./src/types/index.ts) for full interface details.

### PlatformTemplate

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```css
: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:

```typescript
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:

```tsx
// 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`

```tsx
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](./docs/site-injection.md) for full documentation.

---

## Documentation

Detailed guides are available in the `docs/` directory:

| Guide | Description |
|-------|-------------|
| [Architecture](./docs/architecture.md) | System layers, component roles, configuration flow |
| [Site Injection](./docs/site-injection.md) | Actions, zones, badges, level-aware visibility |
| [Direct Component Rendering](./docs/direct-component-rendering.md) | Same-origin widget loading, bare import shimming |
| [Cross-App Communication](./docs/cross-app-communication.md) | postMessage protocols, shared contexts, capability contracts |
| [Authentication](./docs/authentication.md) | AuthKit integration, auth modal, custom auth |
| [Getting Started](./GETTING_STARTED.md) | Lovable initialization prompt for new portal apps |

---

## License

MIT