npm package: @loomio/sdk
Description: React SDK for integrating Loomio CMS with client websites
Author: Loomio
Latest version: 0.1.4

# @loomio/sdk

Framework-agnostic SDK for integrating Loomio CMS with your client websites. This package provides multiple entry points for different use cases: core client, React hooks with server-side rendering support, and server-only utilities.

## Installation

```bash
npm install @loomio/sdk
# or
pnpm add @loomio/sdk
# or
yarn add @loomio/sdk
```

## Package Structure

This SDK is modular with separate entry points:

- `@loomio/sdk` - Core client and global config management
- `@loomio/sdk/react` - React hooks with TanStack Query
- `@loomio/sdk/server` - Server-side utilities with caching support
- `@loomio/sdk/client` - Framework-agnostic client class

## Quick Start

### Global Configuration (Recommended)

Define your configuration once at the root of your application:

```tsx
// lib/loomio-config.ts
import { defineConfig } from '@loomio/sdk';

export const loomioConfig = defineConfig({
  siteId: "your-site-id",
  apiUrl: "https://your-api.example.com", // Optional
});
```

**⚠️ Server-Side Considerations:**
- `defineConfig()` uses a module-level global variable
- Call it ONCE during app initialization, NOT per-request
- In serverless/edge environments, ensure it's set during cold start
- For per-request config changes, use explicit config parameters instead

### React with Global Config

```tsx
// app/layout.tsx
import './lib/loomio-config'; // Import to initialize config
import { LoomioProvider } from '@loomio/sdk/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <LoomioProvider>
          {children}
        </LoomioProvider>
      </body>
    </html>
  );
}
```

```tsx
// components/HomePage.tsx
import { useSite, useGallery, useCustomValue } from '@loomio/sdk/react';

export function HomePage() {
  const { data: site, isLoading } = useSite();
  const { data: gallery } = useGallery();
  const { data: tagline } = useCustomValue('site_tagline');

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{site?.name}</h1>
      <p>{tagline?.value}</p>

      <div className="gallery">
        {gallery?.map(image => (
          <img
            key={image._id}
            src={image.file?.url}
            alt={image.name}
          />
        ))}
      </div>
    </div>
  );
}
```

### Server-Side Rendering (Next.js, etc.)

```tsx
// app/page.tsx
import { getSite, getGallery } from '@loomio/sdk/server';
import './lib/loomio-config'; // Ensure config is loaded

export default async function Page() {
  // Uses global config, with optional overrides
  const site = await getSite({ revalidate: 3600 }); // Revalidate every hour
  const gallery = await getGallery({ tags: ['featured'] }, { revalidate: 300 });

  return (
    <div>
      <h1>{site.name}</h1>
      <div className="gallery">
        {gallery.map(image => (
          <img key={image._id} src={image.file?.url} alt={image.name} />
        ))}
      </div>
    </div>
  );
}
```

### Theme Management

The SDK provides **two-layer theme injection** to eliminate theme flashing while maintaining reactivity:

#### 1. Server-Side Theme Injection (Recommended)

Use the `ThemeScript` component to inject theme variables during server-side rendering, preventing the default theme from flashing:

```tsx
// app/layout.tsx
import { ThemeScript } from '@loomio/sdk/server';
import { LoomioProvider, LoomioTheme } from '@loomio/sdk/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* Inject theme on server (no flash) */}
        <ThemeScript />
        <LoomioProvider>
          {/* Client component syncs theme changes */}
          <LoomioTheme />
          {children}
        </LoomioProvider>
      </body>
    </html>
  );
}
```

**Benefits:**
- ✅ No theme flash on initial load
- ✅ Correct theme in initial HTML (SEO-friendly)
- ✅ Works with SSR/SSG
- ✅ Better performance (no client fetch needed)
- ✅ Zero configuration (uses global config automatically)

**Advanced:** Manual theme injection using `generateThemeStyles()`:

```tsx
import { getSite, generateThemeStyles } from '@loomio/sdk/server';

export default async function RootLayout({ children }) {
  const site = await getSite({ revalidate: 3600 });
  const themeCSS = generateThemeStyles(site.theme);

  return (
    <html>
      <head>
        <style dangerouslySetInnerHTML={{ __html: themeCSS }} />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

#### 2. Client-Side Theme Component

The `LoomioTheme` component handles dynamic theme updates:

```tsx
<LoomioProvider>
  <LoomioTheme />
  {children}
</LoomioProvider>
```

The component:
- Syncs theme changes from the dashboard in real-time
- Handles system preference changes (light/dark mode toggle)
- Updates theme variables without page reload
- Works together with server-side injection

#### Theme Configuration Options

```tsx
<LoomioTheme
  // Respect system dark mode preference (default: true)
  respectSystemPreference={true}

  // Force a specific theme mode
  forceMode="dark"

  // Custom dark mode class name (default: "dark")
  darkModeClass="dark-theme"
/>
```

#### Using Theme Variables

Once injected, you can use the theme variables in your CSS:

```css
.my-component {
  background-color: hsl(var(--background));
  color: hsl(var(--foreground));
  border: 1px solid hsl(var(--border));
}

.button-primary {
  background-color: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
}
```

Available CSS variables include:
- `--background`, `--foreground`
- `--primary`, `--primary-foreground`
- `--secondary`, `--secondary-foreground`
- `--muted`, `--muted-foreground`
- `--accent`, `--accent-foreground`
- `--destructive`, `--destructive-foreground`
- `--border`, `--input`, `--ring`
- `--card`, `--card-foreground`
- `--popover`, `--popover-foreground`
- `--sidebar-*` (various sidebar colors)
- `--chart-1` through `--chart-5`
- `--radius` (border radius)

## Available Hooks

### `useSite()`
Fetches site metadata including name, description, pages, contact info, and social media.

```tsx
const { data: site, isLoading, error } = useSite();
```

### `useGallery(options?)`
Fetches gallery images with optional tag filtering.

```tsx
const { data: images } = useGallery({
  tags: ['featured'],
  enabled: true
});
```

### `useCustomValue(key)`
Fetches a single custom value by key.

```tsx
const { data: value } = useCustomValue('hero_title');
```

### `useCustomValues()`
Fetches all custom values for the site.

```tsx
const { data: values } = useCustomValues();
```

## Configuration

### Configuration Priority

The SDK uses the following priority order for configuration:

1. **Global config** (from `defineConfig()`) merged with explicit overrides
2. **Explicit config** parameter (if it contains `siteId`)
3. **Environment variables** (you can use any environment variable name you prefer)

### Configuration Options

- `siteId` (required): The ID of the site to fetch data for
- `apiUrl` (optional): Your Loomio API base URL. Falls back to environment or defaults

### Alternative: Explicit Config (Without Global Config)

You can also use explicit configuration without global config:

```tsx
// For React
<LoomioProvider
  config={{
    siteId: 'your-site-id',
    apiUrl: 'https://your-api.example.com',
  }}
>
  <YourApp />
</LoomioProvider>
```

```tsx
// For server-side
const site = await getSite({
  siteId: 'your-site-id',
  apiUrl: 'https://your-api.example.com',
  revalidate: 3600,
});
```

### Environment Variables

You can use any environment variable names you prefer. The SDK doesn't enforce specific environment variable names - you configure them when calling `defineConfig()`. Common examples include:

- `NEXT_PUBLIC_LOOMIO_SITE_ID`, `SITE_ID`, `LOOMIO_SITE_ID`, etc. - Your site ID
- `NEXT_PUBLIC_LOOMIO_API_URL`, `API_URL`, `LOOMIO_API_URL`, etc. - Your API URL

Simply pass the environment variable values to `defineConfig()` using whatever variable names fit your project's conventions.

### Multiple Sites

If you need to work with multiple sites in the same application, don't use global config. Instead, pass explicit config to each provider/function call:

```tsx
// Site 1
<LoomioProvider config={{ siteId: 'site-1' }}>
  <Site1Content />
</LoomioProvider>

// Site 2
<LoomioProvider config={{ siteId: 'site-2' }}>
  <Site2Content />
</LoomioProvider>
```

## Features

- 🌐 **Framework-Agnostic**: Core client works with any JavaScript framework
- 🔄 **Reactive Data**: React hooks built on TanStack Query for automatic caching and refetching
- 🎯 **Type-Safe**: Full TypeScript support with generated types
- ⚡ **Optimized**: Smart caching strategies to minimize API calls
- 🚀 **SSR Support**: Server-side rendering with configurable caching (Next.js, Remix, etc.)
- 📦 **Tree-Shakeable**: Modular exports for optimal bundle size
- 🎨 **Flexible**: Global config or explicit config - you choose
- 🔌 **Extensible**: Easy to add custom hooks for your specific needs

## Future Enhancements

This SDK is designed to grow with your needs. Future versions will include:

- Real-time subscriptions via Convex
- Product catalog hooks
- Search functionality
- Pagination support
- Optimistic updates
- And more!

## Development

```bash
# Install dependencies
pnpm install

# Build the package
pnpm build

# Watch mode for development
pnpm dev

# Type checking
pnpm typecheck
```

## License

MIT