# @strata-ds/web

---

> 🌐 **Framework-agnostic web components** for [Strata](https://strata.charisol.io) — connect any frontend to your design system with zero lock-in.

`@strata-ds/web` is a Stencil-powered component library that delivers Strata design tokens and components to **vanilla JS, React, Vue, Angular, Svelte**, and any framework that supports web standards. It ensures design-to-code fidelity across your entire frontend ecosystem.

> Design once in Strata. Ship everywhere.

---

## Specs

Durable, versioned contracts for agent-driven rendering and related runtime
interfaces live in [`docs/specs/`](./docs/specs/README.md).

AI coding agents must read [`AGENTS.md`](./AGENTS.md) before contributing.

---

## Table of Contents

- [Specs](#specs)
- [Why @strata-ds/web?](#why-strataweb)
- [Who Is This For?](#who-is-this-for)
- [Core Features](#core-features)
- [5-Minute Integration Checklist](#-5-minute-integration-checklist)
- [Quick Start](#quick-start--5-minutes)
- [API Reference](#api-reference)
- [Debugging & Troubleshooting](#debugging--troubleshooting)
- [Architecture Overview](#architecture-overview)
- [Authentication](#authentication)
- [Advanced Usage](#advanced-usage)
- [License](#license)
- [Support](#support)

---

## Why @strata-ds/web?

### The Problem

Modern teams use **multiple frameworks** (React for marketing site, Vue for admin panel, vanilla JS for docs). Without a **framework-agnostic design system**, you face:

- Duplicate style implementations across frameworks
- Inconsistent brand experience (React button ≠ Vue button)
- Manual token updates in 5+ repositories
- Designer → developer handoff breaks down per framework

**Key Pain Points:**
- **⚠️ Stale Tokens:** Design tokens become outdated across multiple codebases.
- **❌ No Source of Truth:** No single source of truth for component styling.
- **🔄 Manual Sync:** Manual synchronization between design and code is slow and error-prone.
- **📉 Platform Divergence:** Platform-specific implementations diverge over time.

### The Solution

`@strata-ds/web` uses **Web Components** (browser-native, framework-agnostic) to deliver:

- ✅ **One source of truth** — Tokens and components sync from Strata
- ✅ **Works everywhere** — Vanilla JS, React, Vue, Angular, Svelte, etc.
- ✅ **Real-time updates** — Design changes propagate without redeploying
- ✅ **Zero lock-in** — Web standards, not proprietary APIs
- ✅ **CSS variable injection** for global styling
- ✅ **Dynamic component rendering** based on API-defined structure

---

## Who Is This For?

| Persona | Value |
|---------|-------|
| **Design System Teams** | Single source of truth for variables and components |
| **Solopreneurs** | "I'm using vanilla JS — I don't need React bloat. I just want brand-consistent components." |
| **Frontend Engineers** | "I maintain 3 apps in different frameworks. One design system = one source of truth." |
| **Multi-Framework Teams** | Consistent UI across React, Vue, Angular, and static sites |
| **Product Teams** | Real-time style updates without redeployment |
| **Entrepreneurs** | "My team ships faster because components stay in sync across React, Vue, and marketing pages." |
| **Designers** | "What I create in Strata works everywhere — no framework-specific tweaks." |

---

## Core Features

- 🌐 **Framework-agnostic** — Works in vanilla JS, React, Vue, Angular, Svelte
- 🔁 **Live sync** with Strata backend
- 🧠 **Token-driven styling** — Design tokens as CSS custom properties
- 🧩 **Dynamic component loading** — Fetch components from Strata on-demand
- 🛠 **Framework wrappers** — First-class React and Vue support
- 📴 **Offline-first fallback** — Cached data when network fails
- 🧪 **Debug tools** — `<strata-debug>` component for inspection

---

**Before you start:**
- [ ] Created a project at [strata.charisol.io](https://strata.charisol.io)
- [ ] Imported your design tokens (Style Dictionary JSON or Figma)
- [ ] Published your project (dashboard shows "✅ Published")

**Integration:**
- [ ] Installed `@strata-ds/web` via npm/yarn
- [ ] Copied `projectId` from dashboard URL or project settings
- [ ] Copied `projectToken` from **Publish & Sync** tab (starts with `pt_live_`)
- [ ] Added `<strata-provider>` wrapper
- [ ] Loaded the Stencil script in your HTML/framework entry point
- [ ] Rendered your first component with `<strata-dynamic>`


**Verification:**
- [ ] Opened DevTools → Console and saw `Connected to design system`
- [ ] Console shows `Design system synced: { components: {...}, variables: {...} }`
- [ ] Component renders on screen with correct styles
- [ ] Added `<strata-debug></strata-debug>` and verified connection status

**Stuck?** → Jump to [Debugging & Troubleshooting](#debugging--troubleshooting).

---

## Quick Start (< 5 Minutes)

> 🚀 **New to Strata?** Get your first component rendering in 3 steps.

### Step 1: Install

```bash
npm install @strata-ds/web
# or
yarn add @strata-ds/web
```

### Step 2: Get Your Credentials

Go to [strata.charisol.io](https://strata.charisol.io) → Your Project → Publish & Sync tab:

- Copy your Project ID (e.g., abc123def456)

- Copy your Project Token (starts with pt_live_)


---

### Step 3: Load the Components

#### **Vanilla JS / HTML**

```html
<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@strata-ds/web/dist/strata-web/strata-web.esm.js"></script>
  </head>
  <body>
    <strata-provider
      url="http://localhost:4000"
      enable-realtime="true"
      poll-interval="6000"
      project-id="YOUR_PROJECT_ID"
      project-token="pt_live_your_token_here"
      sync-enabled="true"
    >
      <!-- Dynamic component -->
      <strata-dynamic
        component="button"
        props='{"children": "Click me!", "variant": "primary"}'
      ></strata-dynamic>
    </strata-provider>
  </body>
</html>
```

> 💡 Replace `YOUR_PROJECT_ID` and `pt_live_your_token_here` with values from **Step 2**.

---

#### **React (Next.js / Create React App)**

Step 1: Create a Provider Component

👉 app/providers/StrataProvider.tsx

```tsx
'use client';

import { useEffect, useRef } from 'react';
import { defineCustomElements } from '@strata-ds/web/loader';

export default function StrataProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const providerRef = useRef<any>(null);

  useEffect(() => {
    const load = async () => {
      try {
        await defineCustomElements();
        console.log('✅ Strata web components registered');
      } catch (err) {
        console.error('❌ Failed to register Strata:', err);
      }
    };

    load();
  }, []);

  return (
    <strata-provider
      ref={providerRef}
      url="http://localhost:4000"
      enable-realtime={true}
      poll-interval={6000}
      project-id="YOUR_PROJECT_ID"
      project-token="pt_live_your_token_here"
      sync-enabled={true}
    >
      {children}
    </strata-provider>
  );
}
```

Step 2: Update the Layout (app/layout.tsx)

```tsx
import './globals.css';
import { StrataProvider } from './providers/StrataProvider';
import Script from 'next/script'; // Import the Script component

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        {/* Use the Script component for the CDN */}
        <Script 
          src="https://cdn.tailwindcss.com" 
          strategy="beforeInteractive" 
        />
      </head>
      <body>
        <StrataProvider>
          {children}
        </StrataProvider>
      </body>
    </html>
  );
}
```

Step 3: Usage in Pages (app/page.tsx)

👉 app/page.tsx

```tsx
// app/page.tsx
'use client';

export default function Page() {
  return (
    <main>
      <h1>Strata Design System Test</h1>

      <h2>Card Component</h2>
      <strata-dynamic component="card" sync-enabled={true} />

      <h2>Button Component</h2>
      <strata-dynamic component="button" />
    </main>
  );
}
```

⚠️ 4. Add TypeScript support for custom elements (recommended)

`TypeScript:` If TypeScript complains about the custom tags, ensure you have a custom-elements.d.ts file in your project:

```jsx
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'strata-provider': DetailedHTMLProps<HTMLAttributes<HTMLElement> & {
        url?: string;
        'enable-realtime'?: boolean;
        'poll-interval'?: number;
        'project-id'?: string;
        'sync-enabled'?: boolean;
        'syncToken'?: string;
        'projectToken'?: string;
      }, HTMLElement>;
      'strata-dynamic': DetailedHTMLProps<HTMLAttributes<HTMLElement> & {
        component?: string;
        props?: string;
        'sync-enabled'?: boolean;
      }, HTMLElement>;
      'strata-debug': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
    }
  }
}

export { };
```

Alternative simpler declaration:

```jsx
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'strata-provider': any;
      'strata-dynamic': any;
      'strata-debug': any;
    }
  }
}

export { };
```

---

#### **Vue 3**

```tsx
<template>
  <div>
    <strata-provider 
      url="http://localhost:4000"
      enable-realtime="true"
      poll-interval="6000"
      project-id="YOUR_PROJECT_ID"
      project-token="pt_live_your_token_here"
      sync-enabled="true"
      @connected="onConnected"
      @error="onError"
    >
      <div>
        <h2>Card Component</h2>
        <strata-dynamic component="card" />
        
        <h2>Button Component</h2>
        <strata-dynamic component="button" 
         :props="JSON.stringify({ children: 'Click me!', variant: 'primary' })"
        />
      </div>
    </strata-provider>
  </div>
</template>

<script setup>
const onConnected = () => {
  console.log('Connected to Strata')
}

const onError = (error) => {
  console.error('Strata error:', error)
}
</script>

<style>
body {
  font-family: Arial, sans-serif;
  padding: 20px;
}
</style>
```

Step 2 Update `vite.config.ts` of Vue:

```tsx
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue({
    template: {
      compilerOptions: {
        // Treat all tags with 'strata-' prefix as custom elements
        isCustomElement: (tag) => tag.startsWith('strata-')
      }
    }
  }),
    vueDevTools(),
  ],
  optimizeDeps: {
    include: ['@strata-ds/web', '@strata-ds/web/loader'],
    exclude: []
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
})
```

⚠️ Add TypeScript support for custom elements (recommended)

In your Vue app create a file src/strata-web.d.ts inside it add;

```jsx
declare module '@strata-ds/web/vue' {
  import { Plugin, Component } from 'vue'

  export const StrataProvider: Component
  export const StrataDynamic: Component
  
  const StrataVuePlugin: Plugin
  export default StrataVuePlugin
}
```

Create a new file src/shims-vue.d.ts inside it add;

```jsx
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
```

Inside the index.html of the Vue project add Tailwind CSS CDN

```jsx
       <!-- Add Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
```

---

#### **Angular**

```typescript
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { defineCustomElements } from '@strata-ds/web/loader';

defineCustomElements();

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
```

---

#### **HTML**

```html
<!-- app.component.html -->
<strata-provider
  url="http://localhost:4000"
  enable-realtime="true"
  poll-interval="6000"
  project-id="YOUR_PROJECT_ID"
  project-token="pt_live_your_token_here"
  sync-enabled="true"
>
  <strata-dynamic
    component="Button"
    props='{"children": "Click me!", "variant": "primary"}'
  ></strata-dynamic>
</strata-provider>
```

---

#### **Svelte**

```svelte
<!-- App.svelte -->
<script>
  import { onMount } from 'svelte';
  import { defineCustomElements } from '@strata-ds/web/loader';

  onMount(() => {
    defineCustomElements();
  });
</script>

<strata-provider
  url="http://localhost:4000"
  enable-realtime="true"
  poll-interval="6000"
  project-id="YOUR_PROJECT_ID"
  project-token="pt_live_your_token_here"
  sync-enabled="true"
>
  <strata-dynamic
    component="Button"
    props={JSON.stringify({ children: 'Click me!', variant: 'primary' })}
  ></strata-dynamic>
</strata-provider>
```

---

## API Reference

### `<strata-provider>`

Wraps your app and manages the connection to Strata.

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | `string` | `'http://localhost:4000'` | Base URL for the Strata backend. Use `http://localhost:4000` for local development; set to your deployed Strata backend URL (use `https://` in production to protect tokens) |
| `enable-realtime` | `boolean` | `true` | Enable WebSocket + polling for real-time updates |
| `poll-interval` | `number` | `6000` | Polling interval in milliseconds |
| `project-id` | `string` | `undefined` | Your Strata project ID (from dashboard) |
| `project-token` | `string` | `undefined` | Sync token (starts with `pt_live_`) — required if project is private |
| `sync-enabled` | `boolean` | `true` | Enable/disable syncing |


**Events:**
- `synced` — Fires when design system data syncs successfully
- `error` — Fires when sync fails (check `event.detail.error`)
- `connected` — Fires when WebSocket connects
- `disconnected` - Fired when connection is lost

---

### `<strata-dynamic>`

Renders a component fetched from Strata.

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `component` | `string` | *required* | Name of the component (e.g., `"Button"`, `"Navbar"`) |
| `props` | `string` (JSON) | `'{}'` | Props to pass to the component (JSON string) |
| `sync-enabled` | `boolean` | `true` | Enable/disable syncing for this component |

**Events:**
- `load` — Fires when the component loads successfully
- `error` — Fires when the component fails to load

**Example:**
```html
<strata-dynamic
  component="Card"
  props='{"title": "Welcome", "variant": "elevated"}'
></strata-dynamic>
```

---

### `<strata-debug>`

Renders a debug panel showing connection status and loaded components.

```html
<strata-debug></strata-debug>
```

Displays:
- ✅ Connection status (Connected / Disconnected / Error)
- ✅ Loaded components
- ✅ Loaded variables
- ✅ Sync mode (WebSocket + Polling)
- ✅ Last sync time
- ✅ Any errors

---

## Debugging & Troubleshooting

### Quick Debug Panel

Add `<strata-debug></strata-debug>` anywhere inside `<strata-provider>` to inspect:
- ✅ Connection status
- ✅ Loaded components
- ✅ Loaded design tokens
- ✅ Sync configuration


```html
<strata-provider url="http://localhost:4000" project-id="abc123">
  <strata-debug></strata-debug>
  <!-- Your app content -->
</strata-provider>
```

---

### Console Log Reference

When debugging, look for these messages in DevTools Console:

- ✅ `Strata Web Components loaded` — Script initialized successfully
- ✅ `Connected to design system` — Provider mounted and connected
- ✅ `Design system synced: { components: {...}, variables: {...} }` — Data loaded
- ❌ `401` / `403` — Token or CORS issue (see table above)
- ❌ `Component "X" doesn't exist` — Component name doesn't match your Strata project

---

### Common Issues

| 🚨 Problem | ✅ Solution |
|-----------|-----------|
| **401 Unauthorized** | Your `project-token` is invalid or expired. Go to **Publish & Sync** tab in your Strata project and copy a fresh token. |
| **403 Forbidden** | Your project has CORS restrictions. Add your domain to `allowedOrigins` in your Strata project settings. |
| **Component not rendering** | 1. Check `project-id` matches your dashboard URL<br>2. Verify the component exists in your Strata project<br>3. Open DevTools Console and look for errors |
| **Styles broken / unstyled** | Design tokens are applied as CSS custom properties on `:root`. Check DevTools → Elements → Styles for `--color-*`, `--font-*`, etc. |
| **TypeScript errors (React)** | Install type definitions: `npm install --save-dev @types/react` |
| **Vue warning: unknown element** | Call `defineCustomElements()` from `@strata-ds/web/loader` in your app entry file (see Vue example above) |

---

### Network Tab Debugging

Open DevTools → **Network** tab and filter by:

- **Polling requests** — Look for requests to your `url` (e.g., `http://localhost:4000/sync`); status should be `200 OK` with JSON responses
- **WebSocket connection** — Look for `101 Switching Protocols`

---

## 🏗 Architecture Overview

```
Your App
  └── <strata-provider>        ← Manages connection & token sync
        ├── <strata-dynamic>   ← Fetches & renders individual components
        └── <strata-debug>     ← (Optional) Debug panel
```

---

### @strata-ds/web Core

The following diagram illustrates how `@strata-ds/web` acts as the bridge between the Design System API and the consumer frameworks.

```mermaid
graph TD
    subgraph API ["Design System API"]
        A["Variables"]
        B["Components"]
        C["Real-time WebSocket"]
    end

    API --> Core

    subgraph Core ["@strata-ds/web Core"]
        direction TB
        subgraph Sync ["StrataSystemSyncService - Singleton"]
            S1["- Fetches variables & components from API"]
            S2["- Manages polling/WebSocket connections"]
            S3["- Injects CSS variables into :root"]
            S4["- Event emission for state changes"]
        end

        subgraph Factory ["Component Factory & Style Manager"]
            F1["- Renders components from API definitions"]
            F2["- Processes Tailwind classes and CSS variables"]
            F3["- Generates inline styles from properties"]
        end
    end

    Core --> WC["Web Component Direct Usage"]
    Core --> RW["React Wrapper"]
    Core --> VW["Vue Wrapper"]

    %% Styling
    style API fill:#f9f9f9,stroke:#333,stroke-width:2px
    style Core fill:#282829,stroke:#01579b,stroke-width:2px
    style Sync fill:#333,stroke:#01579b
    style Factory fill:#333,stroke:#01579b
```

## Authentication

If your project is private, pass a `project-token`:

```html
<strata-provider
  project-id="YOUR_PROJECT_ID"
  project-token="pt_live_your_token_here"
>
```

Every sync request will include `Authorization: Bearer pt_live_...` and `X-Project-Token` headers.

1. REST API (Initial Sync)

For standard HTTP requests (fetching variables and component definitions), credentials are sent via Headers:

| Header | Value |
| :--- | :--- |
| **Authorization** | `Bearer pt_live_xxxxxxxxxxxx` |
| **X-Project-Token** | `your-project-id` |


2. WebSocket (Real-time Updates)

For live synchronization, credentials must be passed as Query Parameters to ensure compatibility across all browser environments:

```bash
wss://api.strata.run/sync?token=pt_live_xxx&projectId=your-project-id
```

💡 Why the distinction?

- `Headers` are used for API calls to follow REST best practices and keep tokens out of server access logs.

- `Query Parameters` are used for WebSockets because the initial browser handshake for WSS does not always support custom headers reliably across all platforms (Web, Flutter, etc.).


See how the StrataSystemSyncService handles this internally:

```tsx
// Internal Auth Logic
const headers = {
  'Authorization': `Bearer ${token}`,
  'X-Project-Token': projectId
};

const wsUrl = `${BASE_WS_URL}?token=${token}&projectId=${projectId}`;
```

---

## Advanced Usage

### Listening to Events (Vanilla JS)

```javascript
const provider = document.querySelector('strata-provider');

provider.addEventListener('synced', (event) => {
  console.log('Design system loaded:', event.detail);
});

provider.addEventListener('error', (event) => {
  console.error('Sync failed:', event.detail.error);
});
```


**⚛️ React Usage**

```tsx
import { StrataProvider } from '@strata-ds/web/react';

<StrataProvider
  projectId="YOUR_PROJECT_ID"
  projectToken="pt_live_your_token_here"
  onSync={(data) => console.log('Synced:', data)}
  onError={(error) => console.error('Error:', error)}
>
  {children}
</StrataProvider>
```

### Dynamic Props

Update `props` dynamically:

```javascript
const dynamic = document.querySelector('strata-dynamic');
dynamic.setAttribute('props', JSON.stringify({ variant: 'secondary', children: 'Updated!' }));
```

> ⚠️ **Note:** `props` must be a **JSON string** when using the web component directly. For React, use the `strata-dynamic` wrapper which accepts objects directly.

---

## License

MIT © [Charisol](https://strata.charisol.io)

---

## Resources

- 🌐 [Strata Dashboard](https://strata.charisol.io)
- 📦 [npm Package](https://www.npmjs.com/package/@strata-ds/web)
- 📚 [Stencil Documentation](https://stenciljs.com/)
- 💬 [Community Discord](https://discord.gg/strata) *(placeholder — update with real link)*
