Examples

Recipes

These recipes show patterns for state management, UI layout, and API integration in extension development.

Reference: Current Store & View State

// src/store.ts
import { createStore } from '@stackable-labs/sdk-extension-react'

export type ViewState = { type: 'menu' }

export interface AppState {
  viewState: ViewState
}

export const appStore = createStore<AppState>({
  viewState: { type: 'menu' },
})

Reference: Current Content Surface

// src/surfaces/Content.tsx
import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
import { appStore } from '../store'

export function Content() {
  const viewState = useStore(appStore, (s) => s.viewState)
  const { loading } = useContextData()
  // Non-secret settings from settingsSchema (add settingsSchema to manifest.json to use). Ex:
  // const settings = useSettings()
  // const apiEndpoint = settings.apiEndpoint as string

  if (loading) {
    return (
      <Surface id="slot.content">
        <ui.Stack direction="column" gap="2" className="animate-pulse">
          <ui.Card className="h-24" />
          <ui.Card className="h-32" />
        </ui.Stack>
      </Surface>
    )
  }

  return (
    <Surface id="slot.content">
      {viewState.type === 'menu' && (
        <ui.Menu>
          {/* Add ui.MenuItem entries here */}
        </ui.Menu>
      )}
    </Surface>
  )
}

Reference: Current API Wrapper

// src/lib/api.ts
/**
 * API wrapper patterns — choose one based on your integration model.
 *
 * data.query  → host-mediated: the host handles the API call, extension sends
 *               an action name + params, host returns data.
 *               Permission: "data:query"
 *
 * data.fetch  → direct HTTP: the extension calls external APIs through the
 *               platform proxy. Domains must be in allowedDomains in manifest.
 *               Permission: "data:fetch"
 *
 * Usage in a surface:
 *   const capabilities = useCapabilities()
 *   const api = createApi(capabilities.data.query)
 *   // or: const api = createFetchApi(capabilities.data.fetch)
 */

import type { ApiRequest, FetchRequestInit, FetchResponse } from '@stackable-labs/sdk-extension-contracts'

type QueryFn = <T = unknown>(payload: ApiRequest) => Promise<T>
type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>

// ── data.query wrapper ──────────────────────────────────────────────────────

export function createApi(query: QueryFn) {
  return {
    async getItems(): Promise<unknown[]> {
      return query<unknown[]>({ action: 'getItems' })
    },

    async getItem(itemId: string): Promise<unknown> {
      return query<unknown>({ action: 'getItem', itemId })
    },
  }
}

// ── data.fetch wrapper ──────────────────────────────────────────────────────
//
// For API keys and secrets, use {{settings.xxx}} placeholders in headers.
// The proxy resolves them server-side — the real secret never enters extension code.
// Declare secret fields in manifest.json settingsSchema with "secret": true.

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string

export function createFetchApi(fetch: FetchFn) {
  return {
    async getItems(): Promise<unknown[]> {
      const result = await fetch(`${API_BASE_URL}/items`, {
        method: 'GET',
        headers: {
          'X-API-Key': '{{settings.apiKey}}',
        },
      })
      if (!result.ok) throw new Error(`getItems failed: ${result.status}`)
      return result.data as unknown[]
    },

    async getItem(itemId: string): Promise<unknown> {
      const result = await fetch(`${API_BASE_URL}/items/${itemId}`, {
        method: 'GET',
        headers: {
          'X-API-Key': '{{settings.apiKey}}',
        },
      })
      if (!result.ok) throw new Error(`getItem failed: ${result.status}`)
      return result.data as unknown
    },
  }
}

Auto-generated from Stackable Extension SDK. Questions/Issues? developers@stackablelabs.com

Previous
Patterns
Recipes | Stackable Labs :. Dev Documentation