# Smart Canvas Runtime SDK

## Runtime SDK (React + Shadow DOM)

The SDK ships a framework-agnostic `<smart-canvas>` custom element with an **open shadow root**. We render everything with React, but the canvas is encapsulated so host Tailwind configs, resets, or stacking contexts can't break the UI.

### What's Included

- `SmartCanvasElement` – the custom element that owns the shadow DOM, overlay root, and controller.
- `SmartCanvasApp` – the React tree (overlay, rectangles, toggle) you portal into the shadow.
- `SmartCanvasPortal` – helper for React hosts to portal their tree (and context) into the element mount.
- `createSmartCanvas` – imperative API (`window.SmartCanvas.create`) for non-React hosts.
- **ActionEngine** – unified execution layer for interventions (highlight, tooltip, badge, DOM modifications)
- **Surfaces** – managed surface system for rendering UI into named slots
- **v2 Runtime** – context, events, state, and decisions modules for activation-based rendering
- GrowthBook/PostHog wrappers, hooks, rectangle components, and wheel remain available.

---

## Quick Start

```typescript
import { Syntro } from '@syntrologie/runtime-sdk';

const { runtime } = await Syntro.init({ token: 'syn_...' });

// Change a headline
const handle = await runtime.actions.apply({
  kind: 'set_text',
  anchorId: 'h1.hero-title',
  text: 'Welcome to Our New Experience',
});

// Show a toast notification
const toast = runtime.surfaces.mount('toast_top', {
  type: 'html',
  content: '<div class="toast">Changes applied!</div>',
});

// Later: revert and cleanup
await handle.revert();
toast.unmount();
```

---

## Installation

### NPM/Yarn

```bash
npm install @syntrologie/smart-canvas-sdk
# or
yarn add @syntrologie/smart-canvas-sdk
```

### CDN

For quick setup without a build process:

```html
<!-- Minified production build -->
<script src="https://unpkg.com/@syntrologie/smart-canvas-sdk@latest/dist/smart-canvas.min.js"></script>

<!-- Non-minified development build -->
<script src="https://unpkg.com/@syntrologie/smart-canvas-sdk@latest/dist/smart-canvas.js"></script>

<!-- ES Modules -->
<script type="module">
  import { createSmartCanvas } from 'https://unpkg.com/@syntrologie/smart-canvas-sdk@latest/dist/smart-canvas.esm.js';
</script>
```

### Local Development

```bash
cd packages/runtime-sdk
npm run build
```

---

## Core Modules

### ActionEngine

The ActionEngine provides a unified API for applying reversible interventions:

```typescript
// Visual actions
await runtime.actions.apply({ kind: 'highlight', anchorId: '#cta', style: { color: '#4f46e5' } });
await runtime.actions.apply({
  kind: 'tooltip',
  anchorId: '#help',
  content: { body: 'Click for help' },
});
await runtime.actions.apply({ kind: 'badge', anchorId: '#inbox', content: '3' });
await runtime.actions.apply({ kind: 'pulse', anchorId: '#notification' });

// DOM actions
await runtime.actions.apply({ kind: 'set_text', anchorId: 'h1', text: 'New headline' });
await runtime.actions.apply({
  kind: 'insert_html',
  anchorId: '.cta',
  html: '<span>NEW</span>',
  position: 'append',
});
await runtime.actions.apply({ kind: 'add_class', anchorId: '.card', className: 'highlighted' });
await runtime.actions.apply({
  kind: 'set_style',
  anchorId: '.hero',
  styles: { background: '#1e40af' },
});

// Navigation actions
await runtime.actions.apply({ kind: 'scroll_to', anchorId: '#pricing' });
await runtime.actions.apply({ kind: 'navigate', url: '/signup' });

// Batch with atomic rollback
const batch = await runtime.actions.applyBatch([
  { kind: 'set_text', anchorId: 'h1', text: 'New' },
  { kind: 'highlight', anchorId: 'h1' },
]);
await batch.revertAll();
```

### Surfaces

The Surfaces system manages UI rendering into named slots:

```typescript
// Static slots
runtime.surfaces.mount('toast_top', { type: 'html', content: '<div>Saved!</div>' });
runtime.surfaces.mount('drawer_right', { type: 'html', content: '<div>Settings</div>' });
runtime.surfaces.mount('overlay_center', { type: 'html', content: '<div>Modal</div>' });

// Dynamic slots (positioned relative to anchors)
runtime.surfaces.mount('inline:product-card', { type: 'html', content: '<span>SALE</span>' });
runtime.surfaces.mount(
  'adjacent:help-icon',
  { type: 'html', content: '<div>Help text</div>' },
  {
    position: { placement: 'bottom' },
  }
);

// Priority-based arbitration
runtime.surfaces.mount('toast_top', content, { priority: 10 }); // Higher priority wins
```

**Available Static Slots:**
| Slot | Position |
|------|----------|
| `drawer_right` | Right edge, full height |
| `drawer_left` | Left edge, full height |
| `drawer_bottom` | Bottom edge, full width |
| `overlay_center` | Centered modal |
| `overlay_corner_br` | Bottom-right corner |
| `overlay_corner_bl` | Bottom-left corner |
| `toast_top` | Top center |
| `toast_bottom` | Bottom center |

### Runtime

The runtime provides access to all core providers:

```typescript
const { runtime } = await Syntro.init({ token: 'syn_...' });

runtime.telemetry; // Event tracking (PostHog)
runtime.context; // Page/session/viewport state
runtime.events; // Normalized event stream
runtime.state; // Persistent storage
runtime.actions; // ActionEngine
runtime.surfaces; // Surfaces
```

---

## Using the Custom Element (React Host)

```tsx
import {
  createSyntroExperiment,
  createSyntroTelemetry,
  SmartCanvasApp,
  SmartCanvasPortal,
  registerSmartCanvasElement,
  type SmartCanvasElement,
} from '@syntrologie/smart-canvas-sdk';

const canvasFetcher = async () => {
  /* return ShadowCanvasConfigResponse */
};

function SmartCanvasBridge() {
  const [element, setElement] = useState<SmartCanvasElement | null>(null);

  useEffect(() => {
    registerSmartCanvasElement();
  }, []);

  const controller = element?.getController();

  return (
    <>
      <smart-canvas ref={setElement as any} />
      {element && controller ? (
        <SmartCanvasPortal element={element}>
          <SmartCanvasApp
            controller={controller}
            fetcher={canvasFetcher}
            overlayConfigUri={import.meta.env.VITE_SHADOW_CANVAS_OVERLAY_URI}
          />
        </SmartCanvasPortal>
      ) : null}
    </>
  );
}
```

---

## Using the Imperative API (Plain JS Host)

When using via CDN, the SDK is available as `window.SyntrologieSDK`:

```ts
// Via npm/ES modules
await window.SmartCanvas?.create({
  defaultOpen: true,
  tokens: {
    '--sc-surface': '#050814',
    '--sc-fg': '#f8fafc',
  },
  integrations: {
    posthog, // existing PostHog client
    growthbook, // existing GrowthBook client
  },
});
```

---

## Event System

The EventBus provides a unified stream of normalized events:

```typescript
// Subscribe to events
runtime.events.subscribe({ names: ['ui.click', 'action.applied'] }, (event) => {
  console.log(event.name, event.props);
});

// Standard events
// UI: ui.click, ui.scroll, ui.input, ui.submit
// Navigation: nav.page_view, nav.page_leave
// Canvas: canvas.opened, canvas.closed, tile.viewed, tile.action
// Actions: action.applied, action.reverted, action.failed
// Surfaces: surface.mounted, surface.unmounted
```

---

## State Management

```typescript
// Dismissals
runtime.state.dismissals.mark("promo-banner");
if (runtime.state.dismissals.isDismissed("promo-banner")) { ... }

// Cooldowns
runtime.state.cooldowns.set("upsell", 86400000); // 24h
if (runtime.state.cooldowns.isActive("upsell")) { ... }

// Frequency caps
runtime.state.frequency.increment("tooltip-shown");
if (runtime.state.frequency.count("tooltip-shown") >= 3) { ... }
```

---

## Decision Strategies

Control when adaptives activate:

```typescript
const result = runtime.evaluateSync({
  type: 'rules',
  rules: [
    {
      conditions: [
        { type: 'page_url', pattern: '/pricing*' },
        { type: 'viewport', minWidth: 768 },
        { type: 'dismissed', key: 'promo', inverted: true },
      ],
      value: true,
    },
  ],
  default: false,
});

if (result.value) {
  // Apply adaptive
}
```

---

## Styling & Overlays

- Fonts, colors, and CSS variables are inherited from the host
- Tokens can be overridden via `setTokens` or standard CSS:
  - `--syntro-tooltip-bg`, `--syntro-tooltip-fg`, `--syntro-tooltip-radius`
  - `--syntro-ring`, `--syntro-spotlight-backdrop` for highlights
- A scoped reset is injected via `adoptedStyleSheets`

---

## Schema & Validation

- JSON schema lives at `schema/canvas-config.schema.json`
- Validate a config file locally:

```bash
cd packages/runtime-sdk
npm run validate-config -- path/to/config.json
```

---

## Releasing

The SDK uses [semantic-release](https://github.com/semantic-release/semantic-release) with conventional commits. Versions are never bumped manually.

### Channels

| Channel    | Trigger                                        | npm dist-tag | CDN path                      |
| ---------- | ---------------------------------------------- | ------------ | ----------------------------- |
| **Canary** | Push to `develop` touching `packages/**` | `develop`    | `/v2/canary/`                 |
| **Stable** | Manual workflow dispatch from `main`           | `latest`     | `/v2/latest/` + `/{VERSION}/` |

### Commit Messages → Version Bumps

| Prefix            | Bump                  | Example          |
| ----------------- | --------------------- | ---------------- |
| `fix(sdk): ...`   | Patch (2.0.0 → 2.0.1) | Bug fixes        |
| `feat(sdk): ...`  | Minor (2.0.0 → 2.1.0) | New features     |
| `feat(sdk)!: ...` | Major (2.0.0 → 3.0.0) | Breaking changes |

Commits with `chore`, `docs`, `ci`, or `refactor` prefixes (without `!`) do not trigger a release.

### What the Pipeline Does

1. Builds all adaptives + runtime-sdk (lint, format check, tests)
2. Analyzes commits since last tag → determines version
3. Syncs version to all adaptive `package.json` files + `SDK_VERSION` constant
4. Publishes `runtime-sdk` to npm via OIDC trusted publishing
5. Publishes all `adapt-*` packages to npm via OIDC
6. Deploys CDN bundles to Cloudflare R2
7. Creates GitHub release with changelog

### npm Auth — OIDC Trusted Publishing

No npm tokens are needed. GitHub Actions exchanges a short-lived OIDC token with npm for auth. Each `@syntrologie/*` package has a trusted publisher configured on [npmjs.com](https://www.npmjs.com) pointing to:

- **Org:** `SyntropyForge`
- **Repo:** `amazing-demos`
- **Workflow:** `sdk-release.yaml`

When adding a new package, you must configure its trusted publisher on npmjs.com before the pipeline can publish it. The first publish of a new package requires a manual `npm publish` with a token (OIDC requires the package to already exist).

### Published Packages

| Package                           | Description                                   |
| --------------------------------- | --------------------------------------------- |
| `@syntrologie/runtime-sdk`        | Core runtime                                  |
| `@syntrologie/adapt-content`      | Text, attributes, styles, HTML insertion      |
| `@syntrologie/adapt-overlays`     | Highlights, tooltips, badges, pulse, modals   |
| `@syntrologie/adapt-nav`          | Navigation widget + scrollTo/navigate actions |
| `@syntrologie/adapt-gamification` | Badges, points, leaderboards                  |
| `@syntrologie/adapt-faq`          | FAQ accordion widget                          |
| `@syntrologie/adapt-chatbot`      | Chatbot widget                                |

### CDN URLs

```
# Stable (latest)
https://cdn.syntrologie.com/runtime-sdk/v2/latest/smart-canvas.min.js

# Canary (develop)
https://cdn.syntrologie.com/runtime-sdk/v2/canary/smart-canvas.min.js

# Pinned version
https://cdn.syntrologie.com/runtime-sdk/{VERSION}/smart-canvas.min.js

# Lazy adaptive bundles
https://cdn.syntrologie.com/runtime-sdk/v2/latest/adaptives/{appId}/index.js
```

### Required GitHub Secrets

| Secret                 | Purpose                  |
| ---------------------- | ------------------------ |
| `R2_ACCESS_KEY_ID`     | Cloudflare R2 CDN deploy |
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 CDN deploy |
| `R2_ENDPOINT`          | Cloudflare R2 endpoint   |

`GITHUB_TOKEN` is auto-provided. npm auth is OIDC — no `NPM_TOKEN` needed.

### Troubleshooting

- **"No new version is released"** — 0 relevant commits since last tag. Push a `fix:` or `feat:` commit touching SDK paths.
- **npm 404 / auth error** — package missing trusted publisher config on npmjs.com.
- **Format check fails** — run `npx biome check --fix <file>` locally before pushing.

---

## Documentation

- **[APPS.md](./APPS.md)** - App architecture: how to build runtime extensions with actions, schemas, and editor modules
- **[CAPABILITIES.md](./CAPABILITIES.md)** - Complete reference for all actions, surfaces, and config options
- **[RUNTIME_V2_REFERENCE.md](./RUNTIME_V2_REFERENCE.md)** - Context, Events, State, Decisions modules

---

## Directory Overview

```
packages/runtime-sdk
├─ src
│  ├─ actions/              # ActionEngine (interventions)
│  │  ├─ types.ts
│  │  ├─ ActionEngine.ts
│  │  ├─ validation.ts
│  │  └─ executors/         # visual, dom, navigation executors
│  ├─ apps/                 # App system (see APPS.md)
│  │  ├─ AppRegistry.ts     # App registration and lifecycle
│  │  ├─ AppLoader.ts       # Dynamic app loading
│  │  ├─ types.ts           # AppManifest, AppContext types
│  │  └─ built-in/          # Core apps (bundled with runtime)
│  ├─ surfaces/             # Surfaces (UI rendering)
│  │  ├─ types.ts
│  │  ├─ Surfaces.ts
│  │  └─ positioning.ts
│  ├─ context/              # Page/session/viewport state
│  ├─ events/               # EventBus
│  ├─ state/                # StateStore
│  ├─ decisions/            # DecisionStrategy evaluation
│  ├─ runtime.ts            # SmartCanvasRuntime factory
│  ├─ bootstrap.ts          # Syntro.init()
│  ├─ SmartCanvasElement.tsx
│  ├─ SmartCanvasApp.tsx
│  └─ SmartCanvasPortal.tsx
├─ schema/
│  └─ canvas-config.schema.json
├─ APPS.md
├─ CAPABILITIES.md
├─ RUNTIME_V2_REFERENCE.md
└─ README.md
```

---

## Development Commands

```bash
# Build TypeScript library
npm run build:lib

# Build CDN bundle
npm run build:cdn

# Full build
npm run build

# Validate a config file
npm run validate-config -- path/to/config.json

# Type check
npx tsc --noEmit
```

---

## Testing

- Shadow root is **open**, so Playwright/Cypress can traverse via `.shadow()`
- Data hooks: `[data-shadow-canvas-id="overlay-launcher"]`, `[data-shadow-canvas-id="rectangle-${id}"]`
- `SmartCanvasController` exposes `open/close` for deterministic tests

---

## Analytics Events

The telemetry client emits:

| Event                                           | Description                             |
| ----------------------------------------------- | --------------------------------------- |
| `shadow_canvas_opened` / `shadow_canvas_closed` | Toggle button interactions              |
| `shadow_canvas_rectangle_viewed`                | A rectangle becomes visible             |
| `shadow_canvas_action`                          | CTA click, chatbot send, overlay events |
| `action.applied` / `action.reverted`            | ActionEngine events                     |
| `surface.mounted` / `surface.unmounted`         | Surfaces events                         |

---

## Quick Checklist

1. Call `registerSmartCanvasElement()` once per app
2. Render `<smart-canvas>` (custom element)
3. Portal your React tree into the element's shadow via `SmartCanvasPortal`
4. Configure the SDK with your API credentials
5. Use `runtime.actions` for interventions, `runtime.surfaces` for UI
6. For non-React pages, call `window.SmartCanvas.create()` with configuration
