diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml
new file mode 100644
index 0000000..dbeb918
--- /dev/null
+++ b/.github/workflows/publish-packages.yml
@@ -0,0 +1,154 @@
+name: Publish Framework Packages
+
+on:
+ workflow_dispatch:
+ inputs:
+ package:
+ description: 'Package to publish (react, nextjs, nextjs-server, vue, svelte, or all)'
+ required: true
+ type: choice
+ options:
+ - all
+ - react
+ - nextjs
+ - nextjs-server
+ - vue
+ - svelte
+ version:
+ description: 'Version bump type'
+ required: true
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+ dry-run:
+ description: 'Dry run (do not publish)'
+ required: false
+ type: boolean
+ default: false
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ package: ${{ github.event.inputs.package == 'all' && fromJSON('["react", "nextjs", "nextjs-server", "vue", "svelte"]') || fromJSON(format('["{0}"]', github.event.inputs.package)) }}
+
+ permissions:
+ contents: write
+ packages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
+
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ version: 10
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install root dependencies
+ run: pnpm install --frozen-lockfile || pnpm install
+
+ - name: Install package dependencies
+ working-directory: ./${{ matrix.package }}
+ run: pnpm install || npm install
+
+ - name: Verify package files
+ working-directory: ./${{ matrix.package }}
+ run: |
+ echo "📦 Package contents:"
+ ls -la
+
+ echo ""
+ echo "📄 Files to be published:"
+ npm pack --dry-run 2>&1 | grep -A 100 "package:"
+
+ - name: Bump version
+ working-directory: ./${{ matrix.package }}
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+
+ # Get current version
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
+ echo "Current version: $CURRENT_VERSION"
+
+ # Bump version
+ npm version ${{ github.event.inputs.version }} --no-git-tag-version
+
+ # Get new version
+ NEW_VERSION=$(node -p "require('./package.json').version")
+ echo "New version: $NEW_VERSION"
+ echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
+
+ - name: Verify version update
+ working-directory: ./${{ matrix.package }}
+ run: |
+ echo "✅ Version updated to: ${{ env.NEW_VERSION }}"
+ cat package.json | grep version
+
+ - name: Publish to npm (Dry Run)
+ if: github.event.inputs.dry-run == 'true'
+ working-directory: ./${{ matrix.package }}
+ run: |
+ echo "🔍 Dry run mode - would publish:"
+ npm pack --dry-run
+ ls -la
+
+ - name: Publish to npm
+ if: github.event.inputs.dry-run != 'true'
+ working-directory: ./${{ matrix.package }}
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ # Publish from root directory (includes source files)
+ npm publish --access public
+
+ - name: Commit version bump
+ if: github.event.inputs.dry-run != 'true'
+ run: |
+ git add ${{ matrix.package }}/package.json
+ git commit -m "chore(${{ matrix.package }}): release v${{ env.NEW_VERSION }}" || echo "No changes to commit"
+ git push origin ${{ github.ref_name }} || echo "Nothing to push"
+
+ - name: Create git tag
+ if: github.event.inputs.dry-run != 'true'
+ run: |
+ TAG_NAME="${{ matrix.package }}/v${{ env.NEW_VERSION }}"
+ git tag -a "$TAG_NAME" -m "Release ${{ matrix.package }} v${{ env.NEW_VERSION }}"
+ git push origin "$TAG_NAME"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
+
+ - name: Create GitHub Release
+ if: github.event.inputs.dry-run != 'true'
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ matrix.package }}/v${{ env.NEW_VERSION }}
+ release_name: ${{ matrix.package }} v${{ env.NEW_VERSION }}
+ body: |
+ Release of @zorihq/${{ matrix.package }} v${{ env.NEW_VERSION }}
+
+ ## Installation
+ ```bash
+ npm install @zorihq/${{ matrix.package }}@${{ env.NEW_VERSION }}
+ ```
+
+ See the [README](${{ github.server_url }}/${{ github.repository }}/tree/main/${{ matrix.package }}/README.md) for usage instructions.
+ draft: false
+ prerelease: false
diff --git a/PUBLISHING.md b/PUBLISHING.md
new file mode 100644
index 0000000..ed859a2
--- /dev/null
+++ b/PUBLISHING.md
@@ -0,0 +1,228 @@
+# Publishing Framework Libraries
+
+This document describes how to publish the ZoriHQ framework libraries to npm.
+
+## Prerequisites
+
+### 1. NPM Token
+
+You need to configure an NPM authentication token in GitHub Secrets:
+
+1. Create an npm account at https://www.npmjs.com/
+2. Generate an automation token: https://www.npmjs.com/settings/YOUR_USERNAME/tokens
+3. Add it to GitHub repository secrets as `NPM_TOKEN`
+
+### 2. Package Scope
+
+All packages are published under the `@zorihq` scope:
+- `@zorihq/react`
+- `@zorihq/nextjs`
+- `@zorihq/nextjs-server`
+- `@zorihq/vue`
+- `@zorihq/svelte`
+
+Ensure you have permission to publish to this scope on npm.
+
+## Publishing Workflow
+
+### Automatic Publishing via GitHub Actions
+
+The workflow `publish-packages.yml` handles building, versioning, and publishing packages to npm.
+
+#### Publish a Single Package
+
+1. Go to **Actions** → **Publish Framework Packages**
+2. Click **Run workflow**
+3. Select options:
+ - **Package**: Choose which package to publish (react, nextjs, nextjs-server, vue, svelte)
+ - **Version bump type**: Choose `patch`, `minor`, or `major`
+ - **Dry run**: Check this to test without actually publishing
+
+#### Publish All Packages
+
+1. Go to **Actions** → **Publish Framework Packages**
+2. Click **Run workflow**
+3. Select options:
+ - **Package**: Choose `all`
+ - **Version bump type**: Choose `patch`, `minor`, or `major`
+ - **Dry run**: Check this to test without actually publishing
+
+### What the Workflow Does
+
+1. **Checks out code** from the repository
+2. **Installs dependencies** for the selected package(s)
+3. **Verifies package files** to ensure everything is ready
+4. **Bumps version** according to your selection (patch, minor, major)
+5. **Publishes to npm** with public access
+6. **Commits version bump** back to the repository
+7. **Creates git tag** in format `{package}/v{version}` (e.g., `react/v1.0.1`)
+8. **Creates GitHub Release** with installation instructions
+
+## Manual Publishing (Local)
+
+If you need to publish manually from your local machine:
+
+### 1. Login to npm
+
+```bash
+npm login
+```
+
+### 2. Navigate to Package Directory
+
+```bash
+cd react # or nextjs, nextjs-server, vue, svelte
+```
+
+### 3. Bump Version
+
+```bash
+npm version patch # or minor, major
+```
+
+### 4. Publish
+
+```bash
+npm publish --access public
+```
+
+### 5. Tag and Push
+
+```bash
+git add package.json
+git commit -m "chore(react): release v1.0.1"
+git tag react/v1.0.1
+git push origin main --tags
+```
+
+## Version Bump Guidelines
+
+Follow semantic versioning:
+
+- **patch** (1.0.0 → 1.0.1): Bug fixes, small improvements
+- **minor** (1.0.0 → 1.1.0): New features, backward compatible
+- **major** (1.0.0 → 2.0.0): Breaking changes
+
+## Package Structure
+
+Each package is published with source files (TypeScript/TSX) to allow users' build systems to handle compilation:
+
+```
+@zorihq/react/
+├── index.tsx # Main source file
+├── README.md # Usage documentation
+├── package.json # Package manifest
+└── tsconfig.json # TypeScript config
+```
+
+## Verifying Published Packages
+
+After publishing, verify on npm:
+
+```bash
+npm view @zorihq/react
+npm view @zorihq/nextjs
+npm view @zorihq/nextjs-server
+npm view @zorihq/vue
+npm view @zorihq/svelte
+```
+
+Or visit:
+- https://www.npmjs.com/package/@zorihq/react
+- https://www.npmjs.com/package/@zorihq/nextjs
+- https://www.npmjs.com/package/@zorihq/nextjs-server
+- https://www.npmjs.com/package/@zorihq/vue
+- https://www.npmjs.com/package/@zorihq/svelte
+
+## Testing Before Publishing
+
+### Dry Run with GitHub Actions
+
+Always test with dry-run enabled first:
+
+1. Run the workflow with **dry-run: true**
+2. Check the workflow logs to see what would be published
+3. If everything looks good, run again with **dry-run: false**
+
+### Local Testing
+
+Test package locally before publishing:
+
+```bash
+cd react
+npm pack
+# This creates a .tgz file you can inspect or install locally
+```
+
+Install the packed version in a test project:
+
+```bash
+npm install /path/to/zorihq-react-1.0.0.tgz
+```
+
+## Troubleshooting
+
+### "You do not have permission to publish"
+
+- Ensure you're logged into the correct npm account
+- Ensure you have access to the `@zorihq` scope
+- Check that `NPM_TOKEN` secret is correctly configured
+
+### "Version already exists"
+
+- You're trying to publish a version that's already on npm
+- Bump the version first with `npm version patch/minor/major`
+
+### "Files not included in package"
+
+- Check the `files` field in `package.json`
+- Run `npm pack --dry-run` to see what would be included
+- Verify source files exist in the package directory
+
+## CI/CD Integration
+
+The workflow integrates with GitHub's CI/CD:
+
+- **On workflow_dispatch**: Manual trigger from GitHub Actions UI
+- **Permissions**: Requires `contents: write` and `packages: write`
+- **Matrix strategy**: Can publish multiple packages in parallel when `all` is selected
+
+## Post-Publishing
+
+After publishing:
+
+1. **Update documentation** if needed
+2. **Announce release** in relevant channels
+3. **Monitor npm** for download stats
+4. **Watch for issues** reported by users
+
+## Best Practices
+
+1. ✅ **Always use dry-run first** for new packages
+2. ✅ **Test locally** before publishing
+3. ✅ **Follow semantic versioning** strictly
+4. ✅ **Update README** with breaking changes
+5. ✅ **Tag releases** with descriptive names
+6. ✅ **Keep changelogs** updated
+7. ✅ **Coordinate releases** when updating multiple packages
+
+## Package URLs
+
+After publishing, packages are available at:
+
+- **npm**: `https://www.npmjs.com/package/@zorihq/{package-name}`
+- **GitHub**: `https://github.com/ZoriHQ/script/tree/main/{package-name}`
+- **Unpkg CDN**: `https://unpkg.com/@zorihq/{package-name}` (for bundled builds)
+
+## Support
+
+For issues with publishing:
+
+1. Check GitHub Actions logs for detailed error messages
+2. Verify npm credentials and permissions
+3. Review package.json configuration
+4. Test with `npm pack --dry-run` locally
+
+## License
+
+All packages are published under the MIT License.
diff --git a/nextjs/README.md b/nextjs/README.md
new file mode 100644
index 0000000..de45071
--- /dev/null
+++ b/nextjs/README.md
@@ -0,0 +1,220 @@
+# @zorihq/nextjs
+
+Next.js client-side hooks and components for ZoriHQ Analytics.
+
+## Installation
+
+```bash
+npm install @zorihq/nextjs
+# or
+pnpm add @zorihq/nextjs
+# or
+yarn add @zorihq/nextjs
+```
+
+## Usage with App Router (Next.js 13+)
+
+### 1. Create a Client Component Wrapper
+
+Create `app/providers.tsx`:
+
+```tsx
+'use client';
+
+import { ZoriProvider } from '@zorihq/nextjs';
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+### 2. Wrap Your Root Layout
+
+Update `app/layout.tsx`:
+
+```tsx
+import { Providers } from './providers';
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+### 3. Use in Client Components
+
+```tsx
+'use client';
+
+import { useZori } from '@zorihq/nextjs';
+
+export default function MyPage() {
+ const { track, identify } = useZori();
+
+ const handlePurchase = async () => {
+ await track('purchase_completed', {
+ product_id: 'prod_123',
+ amount: 99.99,
+ });
+ };
+
+ return (
+
+ );
+}
+```
+
+## Usage with Pages Router (Next.js 12)
+
+### 1. Wrap Your App
+
+Update `pages/_app.tsx`:
+
+```tsx
+import { ZoriProvider } from '@zorihq/nextjs';
+import type { AppProps } from 'next/app';
+
+export default function App({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+ );
+}
+```
+
+## Features
+
+### Auto Route Tracking
+
+The `ZoriProvider` automatically tracks route changes in both App Router and Pages Router when `autoTrackPageViews={true}` is set.
+
+### Track Events
+
+```tsx
+'use client';
+
+import { useZori } from '@zorihq/nextjs';
+
+function MyComponent() {
+ const { track } = useZori();
+
+ const handleClick = () => {
+ track('button_clicked', { button_name: 'signup' });
+ };
+
+ return ;
+}
+```
+
+### Identify Users
+
+```tsx
+'use client';
+
+import { useIdentify } from '@zorihq/nextjs';
+
+function UserProfile({ user }) {
+ useIdentify(user ? {
+ app_id: user.id,
+ email: user.email,
+ fullname: user.name,
+ } : null);
+
+ return {user?.name}
;
+}
+```
+
+### TrackClick Component
+
+```tsx
+'use client';
+
+import { TrackClick } from '@zorihq/nextjs';
+
+function MyButton() {
+ return (
+
+ Get Started
+
+ );
+}
+```
+
+## Environment Variables
+
+Create `.env.local`:
+
+```env
+NEXT_PUBLIC_ZORI_KEY=your-publishable-key
+```
+
+## API Reference
+
+### ZoriProvider Props
+
+- `config` (required): Configuration object
+ - `publishableKey` (required): Your ZoriHQ publishable key
+ - `baseUrl` (optional): Custom ingestion endpoint
+ - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms)
+ - `trackQuickSwitches` (optional): Track all visibility changes
+- `children` (required): Your app components
+- `autoTrackPageViews` (optional): Auto-track page views on route change, default `true`
+
+### useZori Hook
+
+Returns an object with:
+
+- `isInitialized`: Boolean indicating if ZoriHQ is ready
+- `track(eventName, properties)`: Track custom events
+- `identify(userInfo)`: Identify users
+- `getVisitorId()`: Get the visitor ID
+- `getSessionId()`: Get the current session ID
+- `setConsent(preferences)`: Set GDPR consent preferences
+- `optOut()`: Opt out of tracking completely
+- `hasConsent()`: Check if user has given consent
+
+## TypeScript Support
+
+This package includes TypeScript definitions out of the box.
+
+## Server-Side Tracking
+
+For server-side tracking (Server Components, API Routes, Middleware), use `@zorihq/nextjs-server` instead.
+
+## License
+
+MIT
diff --git a/nextjs/index.tsx b/nextjs/index.tsx
new file mode 100644
index 0000000..3528494
--- /dev/null
+++ b/nextjs/index.tsx
@@ -0,0 +1,259 @@
+'use client';
+
+import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
+import { usePathname, useSearchParams } from 'next/navigation';
+import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types';
+
+// Re-export shared types for convenience
+export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types';
+
+// Next.js-specific context type extending core API
+export interface ZoriContextType extends ZoriCoreAPI {
+ isInitialized: boolean;
+}
+
+// Context
+const ZoriContext = createContext(null);
+
+// Provider Props
+export interface ZoriProviderProps {
+ config: ZoriConfig;
+ children: React.ReactNode;
+ autoTrackPageViews?: boolean;
+}
+
+// Provider Component
+export const ZoriProvider: React.FC = ({
+ config,
+ children,
+ autoTrackPageViews = true,
+}) => {
+ const [isInitialized, setIsInitialized] = useState(false);
+ const scriptLoadedRef = useRef(false);
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ useEffect(() => {
+ if (scriptLoadedRef.current) return;
+
+ // Initialize queue
+ (window as any).ZoriHQ = (window as any).ZoriHQ || [];
+
+ // Load script
+ const script = document.createElement('script');
+ script.src = 'https://cdn.zorihq.com/script.min.js';
+ script.async = true;
+ script.setAttribute('data-key', config.publishableKey);
+
+ if (config.baseUrl) {
+ script.setAttribute('data-base-url', config.baseUrl);
+ }
+
+ if (config.comebackThreshold !== undefined) {
+ script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString());
+ }
+
+ if (config.trackQuickSwitches !== undefined) {
+ script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString());
+ }
+
+ script.onload = () => {
+ setIsInitialized(true);
+ };
+
+ document.head.appendChild(script);
+ scriptLoadedRef.current = true;
+
+ return () => {
+ if (script.parentNode) {
+ script.parentNode.removeChild(script);
+ }
+ };
+ }, [config]);
+
+ // Auto-track page views on route change (App Router)
+ useEffect(() => {
+ if (isInitialized && autoTrackPageViews) {
+ const zori = (window as any).ZoriHQ;
+ if (zori) {
+ const properties = {
+ page_title: document.title,
+ page_path: pathname,
+ page_search: searchParams?.toString() || '',
+ page_hash: window.location.hash,
+ };
+
+ if (typeof zori.track === 'function') {
+ zori.track('page_view', properties);
+ } else {
+ zori.push(['track', 'page_view', properties]);
+ }
+ }
+ }
+ }, [pathname, searchParams, isInitialized, autoTrackPageViews]);
+
+ const track = useCallback(async (eventName: string, properties?: Record) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.track === 'function') {
+ return await zori.track(eventName, properties);
+ } else {
+ zori.push(['track', eventName, properties]);
+ return true;
+ }
+ }, []);
+
+ const identify = useCallback(async (userInfo: UserInfo) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.identify === 'function') {
+ return await zori.identify(userInfo);
+ } else {
+ zori.push(['identify', userInfo]);
+ return true;
+ }
+ }, []);
+
+ const getVisitorId = useCallback(async () => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return '';
+
+ if (typeof zori.getVisitorId === 'function') {
+ return await zori.getVisitorId();
+ }
+
+ return new Promise((resolve) => {
+ zori.push(['getVisitorId', (id: string) => resolve(id)]);
+ });
+ }, []);
+
+ const getSessionId = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.getSessionId !== 'function') return null;
+ return zori.getSessionId();
+ }, []);
+
+ const setConsent = useCallback((preferences: ConsentPreferences) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.setConsent === 'function') {
+ return zori.setConsent(preferences);
+ } else {
+ zori.push(['setConsent', preferences]);
+ return true;
+ }
+ }, []);
+
+ const optOut = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.optOut === 'function') {
+ return zori.optOut();
+ } else {
+ zori.push(['optOut']);
+ return true;
+ }
+ }, []);
+
+ const hasConsent = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.hasConsent !== 'function') return true;
+ return zori.hasConsent();
+ }, []);
+
+ const contextValue: ZoriContextType = {
+ isInitialized,
+ track,
+ identify,
+ getVisitorId,
+ getSessionId,
+ setConsent,
+ optOut,
+ hasConsent,
+ };
+
+ return {children};
+};
+
+// Hook to use Zori
+export const useZori = (): ZoriContextType => {
+ const context = useContext(ZoriContext);
+ if (!context) {
+ throw new Error('useZori must be used within a ZoriProvider');
+ }
+ return context;
+};
+
+// Hook to track events with dependencies
+export const useTrackEvent = (
+ eventName: string,
+ properties?: Record,
+ dependencies: any[] = []
+) => {
+ const { track, isInitialized } = useZori();
+
+ useEffect(() => {
+ if (isInitialized) {
+ track(eventName, properties);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isInitialized, ...dependencies]);
+};
+
+// Hook to identify user
+export const useIdentify = (userInfo: UserInfo | null) => {
+ const { identify, isInitialized } = useZori();
+
+ useEffect(() => {
+ if (isInitialized && userInfo) {
+ identify(userInfo);
+ }
+ }, [isInitialized, userInfo, identify]);
+};
+
+// Component to track clicks
+export interface TrackClickProps {
+ eventName?: string;
+ properties?: Record;
+ children: React.ReactNode;
+ as?: React.ElementType;
+ [key: string]: any;
+}
+
+export const TrackClick: React.FC = ({
+ eventName = 'click',
+ properties = {},
+ children,
+ as: Component = 'button',
+ ...props
+}) => {
+ const { track } = useZori();
+
+ const handleClick = useCallback(
+ (event: React.MouseEvent) => {
+ track(eventName, properties);
+ if (props.onClick) {
+ props.onClick(event);
+ }
+ },
+ [track, eventName, properties, props]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default {
+ ZoriProvider,
+ useZori,
+ useTrackEvent,
+ useIdentify,
+ TrackClick,
+};
diff --git a/nextjs/package.json b/nextjs/package.json
new file mode 100644
index 0000000..349c17a
--- /dev/null
+++ b/nextjs/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@zorihq/nextjs",
+ "version": "1.0.0",
+ "description": "ZoriHQ Analytics for Next.js (Client-side)",
+ "main": "index.tsx",
+ "types": "index.tsx",
+ "exports": {
+ ".": {
+ "import": "./index.tsx",
+ "require": "./index.tsx",
+ "types": "./index.tsx"
+ }
+ },
+ "files": [
+ "index.tsx",
+ "README.md",
+ "package.json"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "echo 'Publishing source files'",
+ "test": "echo 'Tests not yet configured'"
+ },
+ "keywords": [
+ "analytics",
+ "nextjs",
+ "next",
+ "tracking",
+ "zorihq"
+ ],
+ "author": "ZoriHQ",
+ "license": "MIT",
+ "peerDependencies": {
+ "next": ">=13.0.0",
+ "react": ">=18.0.0"
+ },
+ "dependencies": {
+ "@zorihq/types": "^1.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.0",
+ "next": "^14.0.0",
+ "react": "^18.0.0",
+ "typescript": "^5.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zorihq/script.git",
+ "directory": "nextjs"
+ }
+}
diff --git a/nextjs/pnpm-lock.yaml b/nextjs/pnpm-lock.yaml
new file mode 100644
index 0000000..3a7a328
--- /dev/null
+++ b/nextjs/pnpm-lock.yaml
@@ -0,0 +1,321 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@zorihq/types':
+ specifier: ^1.0.0
+ version: 1.0.0
+ devDependencies:
+ '@types/react':
+ specifier: ^18.0.0
+ version: 18.3.27
+ next:
+ specifier: ^14.0.0
+ version: 14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react:
+ specifier: ^18.0.0
+ version: 18.3.1
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
+packages:
+
+ '@next/env@14.2.33':
+ resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==}
+
+ '@next/swc-darwin-arm64@14.2.33':
+ resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@14.2.33':
+ resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@14.2.33':
+ resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-arm64-musl@14.2.33':
+ resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-x64-gnu@14.2.33':
+ resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-linux-x64-musl@14.2.33':
+ resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-win32-arm64-msvc@14.2.33':
+ resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@next/swc-win32-ia32-msvc@14.2.33':
+ resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@14.2.33':
+ resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+ '@swc/helpers@0.5.5':
+ resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react@18.3.27':
+ resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
+
+ '@zorihq/types@1.0.0':
+ resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==}
+
+ busboy@1.6.0:
+ resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
+ engines: {node: '>=10.16.0'}
+
+ caniuse-lite@1.0.30001756:
+ resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==}
+
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ next@14.2.33:
+ resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==}
+ engines: {node: '>=18.17.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.41.2
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ sass:
+ optional: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ streamsearch@1.1.0:
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
+ engines: {node: '>=10.0.0'}
+
+ styled-jsx@5.1.1:
+ resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+snapshots:
+
+ '@next/env@14.2.33': {}
+
+ '@next/swc-darwin-arm64@14.2.33':
+ optional: true
+
+ '@next/swc-darwin-x64@14.2.33':
+ optional: true
+
+ '@next/swc-linux-arm64-gnu@14.2.33':
+ optional: true
+
+ '@next/swc-linux-arm64-musl@14.2.33':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@14.2.33':
+ optional: true
+
+ '@next/swc-linux-x64-musl@14.2.33':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@14.2.33':
+ optional: true
+
+ '@next/swc-win32-ia32-msvc@14.2.33':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@14.2.33':
+ optional: true
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/helpers@0.5.5':
+ dependencies:
+ '@swc/counter': 0.1.3
+ tslib: 2.8.1
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react@18.3.27':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.2.3
+
+ '@zorihq/types@1.0.0': {}
+
+ busboy@1.6.0:
+ dependencies:
+ streamsearch: 1.1.0
+
+ caniuse-lite@1.0.30001756: {}
+
+ client-only@0.0.1: {}
+
+ csstype@3.2.3: {}
+
+ graceful-fs@4.2.11: {}
+
+ js-tokens@4.0.0: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ nanoid@3.3.11: {}
+
+ next@14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@next/env': 14.2.33
+ '@swc/helpers': 0.5.5
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001756
+ graceful-fs: 4.2.11
+ postcss: 8.4.31
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ styled-jsx: 5.1.1(react@18.3.1)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 14.2.33
+ '@next/swc-darwin-x64': 14.2.33
+ '@next/swc-linux-arm64-gnu': 14.2.33
+ '@next/swc-linux-arm64-musl': 14.2.33
+ '@next/swc-linux-x64-gnu': 14.2.33
+ '@next/swc-linux-x64-musl': 14.2.33
+ '@next/swc-win32-arm64-msvc': 14.2.33
+ '@next/swc-win32-ia32-msvc': 14.2.33
+ '@next/swc-win32-x64-msvc': 14.2.33
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
+ picocolors@1.1.1: {}
+
+ postcss@8.4.31:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
+ source-map-js@1.2.1: {}
+
+ streamsearch@1.1.0: {}
+
+ styled-jsx@5.1.1(react@18.3.1):
+ dependencies:
+ client-only: 0.0.1
+ react: 18.3.1
+
+ tslib@2.8.1: {}
+
+ typescript@5.9.3: {}
diff --git a/nextjs/tsconfig.json b/nextjs/tsconfig.json
new file mode 100644
index 0000000..eee058f
--- /dev/null
+++ b/nextjs/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react",
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["index.tsx"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/react/README.md b/react/README.md
new file mode 100644
index 0000000..3148aff
--- /dev/null
+++ b/react/README.md
@@ -0,0 +1,203 @@
+# @zorihq/react
+
+React hooks and components for ZoriHQ Analytics.
+
+## Installation
+
+```bash
+npm install @zorihq/react
+# or
+pnpm add @zorihq/react
+# or
+yarn add @zorihq/react
+```
+
+## Usage
+
+### 1. Wrap your app with ZoriProvider
+
+```tsx
+import { ZoriProvider } from '@zorihq/react';
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+### 2. Use the hooks
+
+#### useZori - Main Hook
+
+```tsx
+import { useZori } from '@zorihq/react';
+
+function MyComponent() {
+ const { track, identify, getVisitorId, setConsent, optOut } = useZori();
+
+ const handlePurchase = async () => {
+ await track('purchase_completed', {
+ product_id: 'prod_123',
+ amount: 99.99,
+ });
+ };
+
+ const handleLogin = async () => {
+ await identify({
+ app_id: 'user_123',
+ email: 'user@example.com',
+ fullname: 'John Doe',
+ });
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
+
+#### usePageView - Auto Track Page Views
+
+```tsx
+import { usePageView } from '@zorihq/react';
+
+function ProductPage({ productId }) {
+ usePageView({
+ product_id: productId,
+ page_type: 'product',
+ });
+
+ return Product {productId}
;
+}
+```
+
+#### useIdentify - Auto Identify Users
+
+```tsx
+import { useIdentify } from '@zorihq/react';
+
+function UserProfile({ user }) {
+ useIdentify(user ? {
+ app_id: user.id,
+ email: user.email,
+ fullname: user.name,
+ plan: user.subscription
+ } : null);
+
+ return {user?.name}
;
+}
+```
+
+#### useTrackEvent - Track Events with Dependencies
+
+```tsx
+import { useTrackEvent } from '@zorihq/react';
+
+function SearchResults({ query, results }) {
+ useTrackEvent(
+ 'search_completed',
+ {
+ query,
+ result_count: results.length,
+ },
+ [query, results.length] // Re-track when these change
+ );
+
+ return {results.length} results for "{query}"
;
+}
+```
+
+### 3. TrackClick Component
+
+Automatically track clicks on any element:
+
+```tsx
+import { TrackClick } from '@zorihq/react';
+
+function MyButton() {
+ return (
+
+ Sign Up
+
+ );
+}
+
+// Works with any element
+function MyLink() {
+ return (
+
+ Learn More
+
+ );
+}
+```
+
+## API Reference
+
+### ZoriProvider Props
+
+- `config` (required): Configuration object
+ - `publishableKey` (required): Your ZoriHQ publishable key
+ - `baseUrl` (optional): Custom ingestion endpoint
+ - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms)
+ - `trackQuickSwitches` (optional): Track all visibility changes
+- `children` (required): Your app components
+- `autoTrackPageViews` (optional): Auto-track page views on mount, default `true`
+
+### useZori Hook
+
+Returns an object with:
+
+- `isInitialized`: Boolean indicating if ZoriHQ is ready
+- `track(eventName, properties)`: Track custom events
+- `identify(userInfo)`: Identify users
+- `getVisitorId()`: Get the visitor ID
+- `getSessionId()`: Get the current session ID
+- `setConsent(preferences)`: Set GDPR consent preferences
+- `optOut()`: Opt out of tracking completely
+- `hasConsent()`: Check if user has given consent
+
+## TypeScript Support
+
+This package includes TypeScript definitions out of the box.
+
+```tsx
+import { ZoriConfig, UserInfo, ConsentPreferences } from '@zorihq/react';
+
+const config: ZoriConfig = {
+ publishableKey: 'your-key',
+};
+
+const user: UserInfo = {
+ app_id: 'user_123',
+ email: 'user@example.com',
+};
+```
+
+## License
+
+MIT
diff --git a/react/index.tsx b/react/index.tsx
new file mode 100644
index 0000000..960cc32
--- /dev/null
+++ b/react/index.tsx
@@ -0,0 +1,251 @@
+import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
+import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types';
+
+// Re-export shared types for convenience
+export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types';
+
+// React-specific context type extending core API
+export interface ZoriContextType extends ZoriCoreAPI {
+ isInitialized: boolean;
+}
+
+// Context
+const ZoriContext = createContext(null);
+
+// Provider Props
+export interface ZoriProviderProps {
+ config: ZoriConfig;
+ children: React.ReactNode;
+ autoTrackPageViews?: boolean;
+}
+
+// Provider Component
+export const ZoriProvider: React.FC = ({
+ config,
+ children,
+ autoTrackPageViews = true,
+}) => {
+ const [isInitialized, setIsInitialized] = useState(false);
+ const scriptLoadedRef = useRef(false);
+
+ useEffect(() => {
+ if (scriptLoadedRef.current) return;
+
+ // Initialize queue
+ (window as any).ZoriHQ = (window as any).ZoriHQ || [];
+
+ // Load script
+ const script = document.createElement('script');
+ script.src = 'https://cdn.zorihq.com/script.min.js';
+ script.async = true;
+ script.setAttribute('data-key', config.publishableKey);
+
+ if (config.baseUrl) {
+ script.setAttribute('data-base-url', config.baseUrl);
+ }
+
+ if (config.comebackThreshold !== undefined) {
+ script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString());
+ }
+
+ if (config.trackQuickSwitches !== undefined) {
+ script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString());
+ }
+
+ script.onload = () => {
+ setIsInitialized(true);
+ };
+
+ document.head.appendChild(script);
+ scriptLoadedRef.current = true;
+
+ return () => {
+ if (script.parentNode) {
+ script.parentNode.removeChild(script);
+ }
+ };
+ }, [config]);
+
+ const track = useCallback(async (eventName: string, properties?: Record) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.track === 'function') {
+ return await zori.track(eventName, properties);
+ } else {
+ zori.push(['track', eventName, properties]);
+ return true;
+ }
+ }, []);
+
+ const identify = useCallback(async (userInfo: UserInfo) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.identify === 'function') {
+ return await zori.identify(userInfo);
+ } else {
+ zori.push(['identify', userInfo]);
+ return true;
+ }
+ }, []);
+
+ const getVisitorId = useCallback(async () => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return '';
+
+ if (typeof zori.getVisitorId === 'function') {
+ return await zori.getVisitorId();
+ }
+
+ return new Promise((resolve) => {
+ zori.push(['getVisitorId', (id: string) => resolve(id)]);
+ });
+ }, []);
+
+ const getSessionId = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.getSessionId !== 'function') return null;
+ return zori.getSessionId();
+ }, []);
+
+ const setConsent = useCallback((preferences: ConsentPreferences) => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.setConsent === 'function') {
+ return zori.setConsent(preferences);
+ } else {
+ zori.push(['setConsent', preferences]);
+ return true;
+ }
+ }, []);
+
+ const optOut = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.optOut === 'function') {
+ return zori.optOut();
+ } else {
+ zori.push(['optOut']);
+ return true;
+ }
+ }, []);
+
+ const hasConsent = useCallback(() => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.hasConsent !== 'function') return true;
+ return zori.hasConsent();
+ }, []);
+
+ const contextValue: ZoriContextType = {
+ isInitialized,
+ track,
+ identify,
+ getVisitorId,
+ getSessionId,
+ setConsent,
+ optOut,
+ hasConsent,
+ };
+
+ return {children};
+};
+
+// Hook to use Zori
+export const useZori = (): ZoriContextType => {
+ const context = useContext(ZoriContext);
+ if (!context) {
+ throw new Error('useZori must be used within a ZoriProvider');
+ }
+ return context;
+};
+
+// Hook to track page views
+export const usePageView = (properties?: Record) => {
+ const { track, isInitialized } = useZori();
+
+ useEffect(() => {
+ if (isInitialized) {
+ track('page_view', {
+ page_title: document.title,
+ page_path: window.location.pathname,
+ page_search: window.location.search,
+ page_hash: window.location.hash,
+ ...properties,
+ });
+ }
+ }, [isInitialized, track, properties]);
+};
+
+// Hook to track events with dependencies
+export const useTrackEvent = (
+ eventName: string,
+ properties?: Record,
+ dependencies: any[] = []
+) => {
+ const { track, isInitialized } = useZori();
+
+ useEffect(() => {
+ if (isInitialized) {
+ track(eventName, properties);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isInitialized, ...dependencies]);
+};
+
+// Hook to identify user
+export const useIdentify = (userInfo: UserInfo | null) => {
+ const { identify, isInitialized } = useZori();
+
+ useEffect(() => {
+ if (isInitialized && userInfo) {
+ identify(userInfo);
+ }
+ }, [isInitialized, userInfo, identify]);
+};
+
+// Component to track clicks
+export interface TrackClickProps {
+ eventName?: string;
+ properties?: Record;
+ children: React.ReactNode;
+ as?: React.ElementType;
+ [key: string]: any;
+}
+
+export const TrackClick: React.FC = ({
+ eventName = 'click',
+ properties = {},
+ children,
+ as: Component = 'button',
+ ...props
+}) => {
+ const { track } = useZori();
+
+ const handleClick = useCallback(
+ (event: React.MouseEvent) => {
+ track(eventName, properties);
+ if (props.onClick) {
+ props.onClick(event);
+ }
+ },
+ [track, eventName, properties, props]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default {
+ ZoriProvider,
+ useZori,
+ usePageView,
+ useTrackEvent,
+ useIdentify,
+ TrackClick,
+};
diff --git a/react/package.json b/react/package.json
new file mode 100644
index 0000000..eb62969
--- /dev/null
+++ b/react/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@zorihq/react",
+ "version": "1.0.0",
+ "description": "ZoriHQ Analytics for React",
+ "main": "index.tsx",
+ "types": "index.tsx",
+ "exports": {
+ ".": {
+ "import": "./index.tsx",
+ "require": "./index.tsx",
+ "types": "./index.tsx"
+ }
+ },
+ "files": [
+ "index.tsx",
+ "README.md",
+ "package.json"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "echo 'Publishing source files'",
+ "test": "echo 'Tests not yet configured'"
+ },
+ "keywords": [
+ "analytics",
+ "react",
+ "tracking",
+ "zorihq"
+ ],
+ "author": "ZoriHQ",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "dependencies": {
+ "@zorihq/types": "^1.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.0",
+ "react": "^18.0.0",
+ "typescript": "^5.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zorihq/script.git",
+ "directory": "react"
+ }
+}
diff --git a/react/pnpm-lock.yaml b/react/pnpm-lock.yaml
new file mode 100644
index 0000000..15f49cc
--- /dev/null
+++ b/react/pnpm-lock.yaml
@@ -0,0 +1,78 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@zorihq/types':
+ specifier: ^1.0.0
+ version: 1.0.0
+ devDependencies:
+ '@types/react':
+ specifier: ^18.0.0
+ version: 18.3.27
+ react:
+ specifier: ^18.0.0
+ version: 18.3.1
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
+packages:
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react@18.3.27':
+ resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
+
+ '@zorihq/types@1.0.0':
+ resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+snapshots:
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react@18.3.27':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.2.3
+
+ '@zorihq/types@1.0.0': {}
+
+ csstype@3.2.3: {}
+
+ js-tokens@4.0.0: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ typescript@5.9.3: {}
diff --git a/react/tsconfig.json b/react/tsconfig.json
new file mode 100644
index 0000000..eee058f
--- /dev/null
+++ b/react/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react",
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["index.tsx"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/shared/package.json b/shared/package.json
new file mode 100644
index 0000000..42f6b0c
--- /dev/null
+++ b/shared/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@zorihq/types",
+ "version": "1.0.0",
+ "description": "Shared TypeScript types for ZoriHQ Analytics SDKs",
+ "main": "types.ts",
+ "types": "types.ts",
+ "exports": {
+ ".": {
+ "import": "./types.ts",
+ "require": "./types.ts",
+ "types": "./types.ts"
+ }
+ },
+ "files": [
+ "types.ts",
+ "package.json"
+ ],
+ "scripts": {
+ "prepublishOnly": "echo 'Publishing source files'"
+ },
+ "keywords": [
+ "analytics",
+ "types",
+ "typescript",
+ "zorihq"
+ ],
+ "author": "ZoriHQ",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zorihq/script.git",
+ "directory": "shared"
+ }
+}
diff --git a/shared/types.ts b/shared/types.ts
new file mode 100644
index 0000000..f640cd0
--- /dev/null
+++ b/shared/types.ts
@@ -0,0 +1,132 @@
+/**
+ * Shared TypeScript types for ZoriHQ SDKs
+ * These types are reused across React, Next.js, Vue, Svelte, and server-side implementations
+ */
+
+// =============================================================================
+// Core Configuration Types
+// =============================================================================
+
+/**
+ * Configuration options for ZoriHQ client-side SDKs
+ */
+export interface ZoriConfig {
+ /** Your ZoriHQ publishable key */
+ publishableKey: string;
+ /** Custom API base URL (optional) */
+ baseUrl?: string;
+ /** Threshold in ms for detecting user comebacks (optional) */
+ comebackThreshold?: number;
+ /** Whether to track quick tab switches (optional) */
+ trackQuickSwitches?: boolean;
+}
+
+/**
+ * Configuration options for ZoriHQ server-side SDK
+ * A subset of ZoriConfig without client-specific options
+ */
+export interface ZoriServerConfig {
+ /** Your ZoriHQ publishable key */
+ publishableKey: string;
+ /** Custom API base URL (optional) */
+ baseUrl?: string;
+}
+
+// =============================================================================
+// User & Consent Types
+// =============================================================================
+
+/**
+ * User consent preferences for tracking
+ */
+export interface ConsentPreferences {
+ /** Whether analytics tracking is allowed */
+ analytics?: boolean;
+ /** Whether marketing tracking is allowed */
+ marketing?: boolean;
+}
+
+/**
+ * User identification information
+ */
+export interface UserInfo {
+ /** Application-specific user ID */
+ app_id?: string;
+ /** User's email address */
+ email?: string;
+ /** User's full name (alternative 1) */
+ fullname?: string;
+ /** User's full name (alternative 2) */
+ full_name?: string;
+ /** Additional custom properties */
+ [key: string]: any;
+}
+
+// =============================================================================
+// Core API Interface
+// =============================================================================
+
+/**
+ * Core API methods shared across all ZoriHQ client SDKs
+ * Framework-specific implementations extend or implement this interface
+ */
+export interface ZoriCoreAPI {
+ /** Track a custom event */
+ track: (eventName: string, properties?: Record) => Promise;
+ /** Identify a user */
+ identify: (userInfo: UserInfo) => Promise;
+ /** Get the current visitor ID */
+ getVisitorId: () => Promise;
+ /** Get the current session ID */
+ getSessionId: () => string | null;
+ /** Set user consent preferences */
+ setConsent: (preferences: ConsentPreferences) => boolean;
+ /** Opt out of all tracking */
+ optOut: () => boolean;
+ /** Check if user has given consent */
+ hasConsent: () => boolean;
+}
+
+// =============================================================================
+// Server-Side Types
+// =============================================================================
+
+/**
+ * Options for tracking events on the server side
+ */
+export interface TrackEventOptions {
+ /** Name of the event to track */
+ eventName: string;
+ /** Custom properties to attach to the event */
+ properties?: Record;
+ /** Override visitor ID (optional, auto-generated if not provided) */
+ visitorId?: string;
+ /** Override session ID (optional, auto-generated if not provided) */
+ sessionId?: string;
+ /** User agent string */
+ userAgent?: string;
+ /** Full page URL */
+ pageUrl?: string;
+ /** Host/domain name */
+ host?: string;
+ /** Referrer URL */
+ referrer?: string;
+}
+
+/**
+ * Options for identifying users on the server side
+ */
+export interface IdentifyOptions {
+ /** User information to associate with the visitor */
+ userInfo: UserInfo;
+ /** Override visitor ID (optional, auto-generated if not provided) */
+ visitorId?: string;
+ /** Override session ID (optional, auto-generated if not provided) */
+ sessionId?: string;
+ /** User agent string */
+ userAgent?: string;
+ /** Full page URL */
+ pageUrl?: string;
+ /** Host/domain name */
+ host?: string;
+}
diff --git a/svelte/README.md b/svelte/README.md
new file mode 100644
index 0000000..c1f0510
--- /dev/null
+++ b/svelte/README.md
@@ -0,0 +1,333 @@
+# @zorihq/svelte
+
+Svelte stores, actions, and components for ZoriHQ Analytics.
+
+## Installation
+
+```bash
+npm install @zorihq/svelte
+# or
+pnpm add @zorihq/svelte
+# or
+yarn add @zorihq/svelte
+```
+
+## Usage
+
+### 1. Initialize Zori
+
+#### Option A: Global Store (Recommended)
+
+Initialize once in your root component or layout:
+
+```svelte
+
+
+
+
+```
+
+#### Option B: Component Context
+
+Use the `ZoriProvider` component:
+
+```svelte
+
+
+
+
+
+
+```
+
+### 2. Track Events
+
+```svelte
+
+
+
+
+```
+
+### 3. Use Actions
+
+#### trackClick Action
+
+Automatically track clicks on any element:
+
+```svelte
+
+
+
+
+
+ View Pricing
+
+```
+
+### 4. Use Helpers
+
+#### usePageView
+
+Auto-track page views on component mount:
+
+```svelte
+
+
+Product {productId}
+```
+
+#### useTrackEvent
+
+Track events on component mount:
+
+```svelte
+
+
+User Profile
+```
+
+#### useIdentify
+
+Identify users on component mount:
+
+```svelte
+
+
+{user?.name}
+```
+
+### 5. Reactive Tracking
+
+```svelte
+
+
+
+{results.length} results
+```
+
+### 6. Check Initialization Status
+
+```svelte
+
+
+{#if $zori.isInitialized}
+ Analytics Ready!
+{:else}
+ Loading analytics...
+{/if}
+```
+
+## SvelteKit Integration
+
+### +layout.svelte
+
+```svelte
+
+
+
+```
+
+### Environment Variables
+
+Create `.env`:
+
+```env
+VITE_ZORI_KEY=your-publishable-key
+```
+
+## API Reference
+
+### createZoriStore(config)
+
+Create a new Zori store instance.
+
+```typescript
+import { createZoriStore } from '@zorihq/svelte';
+
+const zori = createZoriStore({
+ publishableKey: 'your-key',
+ baseUrl: 'https://ingestion.zorihq.com/ingest', // optional
+ comebackThreshold: 30000, // optional
+ trackQuickSwitches: false, // optional
+});
+```
+
+### initZori(config)
+
+Initialize global Zori store (recommended for app-wide usage).
+
+```typescript
+import { initZori } from '@zorihq/svelte';
+
+const zori = initZori({ publishableKey: 'your-key' });
+```
+
+### getZori()
+
+Get the global Zori store instance.
+
+```typescript
+import { getZori } from '@zorihq/svelte';
+
+const zori = getZori();
+```
+
+### ZoriStore
+
+The store exposes:
+
+- `isInitialized`: Readable store indicating if ZoriHQ is ready
+- `track(eventName, properties)`: Track custom events
+- `identify(userInfo)`: Identify users
+- `getVisitorId()`: Get the visitor ID
+- `getSessionId()`: Get the current session ID
+- `setConsent(preferences)`: Set GDPR consent preferences
+- `optOut()`: Opt out of tracking completely
+- `hasConsent()`: Check if user has given consent
+
+### Actions
+
+#### trackClick
+
+```svelte
+
+```
+
+### Helpers
+
+- `usePageView(properties?)`: Track page view on mount
+- `useTrackEvent(eventName, properties?)`: Track event on mount
+- `useIdentify(userInfo)`: Identify user on mount
+
+## TypeScript Support
+
+This package includes full TypeScript support:
+
+```typescript
+import type {
+ ZoriConfig,
+ ZoriStore,
+ ConsentPreferences,
+ UserInfo,
+} from '@zorihq/svelte';
+```
+
+## Stores Pattern
+
+This library follows Svelte's stores pattern:
+
+- Use `$` to auto-subscribe in components
+- Stores are reactive and update automatically
+- Automatic cleanup on component destroy
+
+## License
+
+MIT
diff --git a/svelte/ZoriProvider.svelte b/svelte/ZoriProvider.svelte
new file mode 100644
index 0000000..f5daad8
--- /dev/null
+++ b/svelte/ZoriProvider.svelte
@@ -0,0 +1,14 @@
+
+
+
diff --git a/svelte/index.ts b/svelte/index.ts
new file mode 100644
index 0000000..fa2900b
--- /dev/null
+++ b/svelte/index.ts
@@ -0,0 +1,236 @@
+import { writable, readonly, derived, get, type Readable, type Writable } from 'svelte/store';
+import { onMount, onDestroy } from 'svelte';
+import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types';
+
+// Re-export shared types for convenience
+export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types';
+
+// Svelte-specific store type extending core API with reactive state
+export interface ZoriStore extends Omit {
+ isInitialized: Readable;
+ getSessionId: () => string | null;
+}
+
+// Create the Zori store
+export function createZoriStore(config: ZoriConfig): ZoriStore {
+ const isInitialized = writable(false);
+ let scriptLoaded = false;
+
+ const loadScript = () => {
+ if (scriptLoaded || typeof window === 'undefined') return;
+
+ // Initialize queue
+ (window as any).ZoriHQ = (window as any).ZoriHQ || [];
+
+ // Load script
+ const script = document.createElement('script');
+ script.src = 'https://cdn.zorihq.com/script.min.js';
+ script.async = true;
+ script.setAttribute('data-key', config.publishableKey);
+
+ if (config.baseUrl) {
+ script.setAttribute('data-base-url', config.baseUrl);
+ }
+
+ if (config.comebackThreshold !== undefined) {
+ script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString());
+ }
+
+ if (config.trackQuickSwitches !== undefined) {
+ script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString());
+ }
+
+ script.onload = () => {
+ isInitialized.set(true);
+ };
+
+ document.head.appendChild(script);
+ scriptLoaded = true;
+ };
+
+ const track = async (eventName: string, properties?: Record): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.track === 'function') {
+ return await zori.track(eventName, properties);
+ } else {
+ zori.push(['track', eventName, properties]);
+ return true;
+ }
+ };
+
+ const identify = async (userInfo: UserInfo): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.identify === 'function') {
+ return await zori.identify(userInfo);
+ } else {
+ zori.push(['identify', userInfo]);
+ return true;
+ }
+ };
+
+ const getVisitorId = async (): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return '';
+
+ if (typeof zori.getVisitorId === 'function') {
+ return await zori.getVisitorId();
+ }
+
+ return new Promise((resolve) => {
+ zori.push(['getVisitorId', (id: string) => resolve(id)]);
+ });
+ };
+
+ const getSessionId = (): string | null => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.getSessionId !== 'function') return null;
+ return zori.getSessionId();
+ };
+
+ const setConsent = (preferences: ConsentPreferences): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.setConsent === 'function') {
+ return zori.setConsent(preferences);
+ } else {
+ zori.push(['setConsent', preferences]);
+ return true;
+ }
+ };
+
+ const optOut = (): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.optOut === 'function') {
+ return zori.optOut();
+ } else {
+ zori.push(['optOut']);
+ return true;
+ }
+ };
+
+ const hasConsent = (): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.hasConsent !== 'function') return true;
+ return zori.hasConsent();
+ };
+
+ // Load script immediately
+ if (typeof window !== 'undefined') {
+ loadScript();
+ }
+
+ return {
+ isInitialized: readonly(isInitialized),
+ track,
+ identify,
+ getVisitorId,
+ getSessionId,
+ setConsent,
+ optOut,
+ hasConsent,
+ };
+}
+
+// Global store instance (optional, for app-wide usage)
+let globalStore: ZoriStore | null = null;
+
+export function initZori(config: ZoriConfig): ZoriStore {
+ if (!globalStore) {
+ globalStore = createZoriStore(config);
+ }
+ return globalStore;
+}
+
+export function getZori(): ZoriStore {
+ if (!globalStore) {
+ throw new Error('Zori not initialized. Call initZori(config) first.');
+ }
+ return globalStore;
+}
+
+// Action: trackClick
+export function trackClick(
+ node: HTMLElement,
+ options: { eventName?: string; properties?: Record } = {}
+) {
+ const handleClick = async () => {
+ const store = getZori();
+ await store.track(options.eventName || 'click', options.properties || {});
+ };
+
+ node.addEventListener('click', handleClick);
+
+ return {
+ destroy() {
+ node.removeEventListener('click', handleClick);
+ },
+ };
+}
+
+// Helper: usePageView (for components)
+export function usePageView(properties?: Record) {
+ const store = getZori();
+
+ onMount(() => {
+ const unsubscribe = store.isInitialized.subscribe((initialized) => {
+ if (initialized) {
+ store.track('page_view', {
+ page_title: document.title,
+ page_path: window.location.pathname,
+ page_search: window.location.search,
+ page_hash: window.location.hash,
+ ...properties,
+ });
+ unsubscribe();
+ }
+ });
+ });
+}
+
+// Helper: useTrackEvent (for components)
+export function useTrackEvent(eventName: string, properties?: Record) {
+ const store = getZori();
+
+ onMount(() => {
+ const unsubscribe = store.isInitialized.subscribe((initialized) => {
+ if (initialized) {
+ store.track(eventName, properties);
+ unsubscribe();
+ }
+ });
+ });
+}
+
+// Helper: useIdentify (for components)
+export function useIdentify(userInfo: UserInfo | null) {
+ const store = getZori();
+
+ onMount(() => {
+ if (!userInfo) return;
+
+ const unsubscribe = store.isInitialized.subscribe((initialized) => {
+ if (initialized) {
+ store.identify(userInfo);
+ unsubscribe();
+ }
+ });
+ });
+}
+
+// Export default
+export default {
+ createZoriStore,
+ initZori,
+ getZori,
+ trackClick,
+ usePageView,
+ useTrackEvent,
+ useIdentify,
+};
diff --git a/svelte/package.json b/svelte/package.json
new file mode 100644
index 0000000..0a02e01
--- /dev/null
+++ b/svelte/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@zorihq/svelte",
+ "version": "1.0.0",
+ "description": "ZoriHQ Analytics for Svelte",
+ "main": "index.ts",
+ "svelte": "index.ts",
+ "types": "index.ts",
+ "exports": {
+ ".": {
+ "import": "./index.ts",
+ "require": "./index.ts",
+ "types": "./index.ts",
+ "svelte": "./index.ts"
+ },
+ "./ZoriProvider.svelte": "./ZoriProvider.svelte"
+ },
+ "files": [
+ "index.ts",
+ "ZoriProvider.svelte",
+ "README.md",
+ "package.json"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "echo 'Publishing source files'",
+ "test": "echo 'Tests not yet configured'"
+ },
+ "keywords": [
+ "analytics",
+ "svelte",
+ "sveltekit",
+ "tracking",
+ "zorihq"
+ ],
+ "author": "ZoriHQ",
+ "license": "MIT",
+ "peerDependencies": {
+ "svelte": ">=3.0.0"
+ },
+ "dependencies": {
+ "@zorihq/types": "^1.0.0"
+ },
+ "devDependencies": {
+ "svelte": "^4.0.0",
+ "typescript": "^5.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zorihq/script.git",
+ "directory": "svelte"
+ }
+}
diff --git a/svelte/pnpm-lock.yaml b/svelte/pnpm-lock.yaml
new file mode 100644
index 0000000..cf485d1
--- /dev/null
+++ b/svelte/pnpm-lock.yaml
@@ -0,0 +1,183 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@zorihq/types':
+ specifier: ^1.0.0
+ version: 1.0.0
+ devDependencies:
+ svelte:
+ specifier: ^4.0.0
+ version: 4.2.20
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@zorihq/types@1.0.0':
+ resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==}
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ code-red@1.0.4:
+ resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
+
+ css-tree@2.3.1:
+ resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ is-reference@3.0.3:
+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+
+ locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ mdn-data@2.0.30:
+ resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+
+ periscopic@3.1.0:
+ resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ svelte@4.2.20:
+ resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==}
+ engines: {node: '>=16'}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@types/estree@1.0.8': {}
+
+ '@zorihq/types@1.0.0': {}
+
+ acorn@8.15.0: {}
+
+ aria-query@5.3.2: {}
+
+ axobject-query@4.1.0: {}
+
+ code-red@1.0.4:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@types/estree': 1.0.8
+ acorn: 8.15.0
+ estree-walker: 3.0.3
+ periscopic: 3.1.0
+
+ css-tree@2.3.1:
+ dependencies:
+ mdn-data: 2.0.30
+ source-map-js: 1.2.1
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ is-reference@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ locate-character@3.0.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ mdn-data@2.0.30: {}
+
+ periscopic@3.1.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-walker: 3.0.3
+ is-reference: 3.0.3
+
+ source-map-js@1.2.1: {}
+
+ svelte@4.2.20:
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+ '@types/estree': 1.0.8
+ acorn: 8.15.0
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ code-red: 1.0.4
+ css-tree: 2.3.1
+ estree-walker: 3.0.3
+ is-reference: 3.0.3
+ locate-character: 3.0.0
+ magic-string: 0.30.21
+ periscopic: 3.1.0
+
+ typescript@5.9.3: {}
diff --git a/svelte/tsconfig.json b/svelte/tsconfig.json
new file mode 100644
index 0000000..23585ec
--- /dev/null
+++ b/svelte/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["index.ts"],
+ "exclude": ["node_modules", "dist", "*.svelte"]
+}
diff --git a/vue/README.md b/vue/README.md
new file mode 100644
index 0000000..71b97c7
--- /dev/null
+++ b/vue/README.md
@@ -0,0 +1,274 @@
+# @zorihq/vue
+
+Vue 3 composables and plugin for ZoriHQ Analytics.
+
+## Installation
+
+```bash
+npm install @zorihq/vue
+# or
+pnpm add @zorihq/vue
+# or
+yarn add @zorihq/vue
+```
+
+## Usage
+
+### 1. Install the Plugin
+
+```typescript
+// main.ts
+import { createApp } from 'vue';
+import { ZoriPlugin } from '@zorihq/vue';
+import App from './App.vue';
+import router from './router'; // Optional: for auto-tracking
+
+const app = createApp(App);
+
+app.use(ZoriPlugin, {
+ config: {
+ publishableKey: 'your-publishable-key',
+ baseUrl: 'https://ingestion.zorihq.com/ingest', // optional
+ comebackThreshold: 30000, // optional
+ trackQuickSwitches: false, // optional
+ },
+ router, // Optional: pass Vue Router for auto page view tracking
+ autoTrackPageViews: true, // Optional: default true
+});
+
+app.use(router);
+app.mount('#app');
+```
+
+### 2. Use the Composables
+
+#### useZori - Main Composable
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+#### usePageView - Auto Track Page Views
+
+```vue
+
+
+
+ Product {{ productId }}
+
+```
+
+#### useIdentify - Auto Identify Users
+
+```vue
+
+
+
+ {{ user?.name }}
+
+```
+
+#### useTrackEvent - Track Events with Reactivity
+
+```vue
+
+
+
+
+
+
{{ results.length }} results for "{{ query }}"
+
+
+```
+
+### 3. Template Usage with v-on
+
+```vue
+
+
+
+
+
+```
+
+## Vue Router Integration
+
+When you pass the Vue Router instance to the plugin, it automatically tracks page views on route changes:
+
+```typescript
+app.use(ZoriPlugin, {
+ config: { publishableKey: 'your-key' },
+ router, // Auto-tracks page views
+ autoTrackPageViews: true, // Enable/disable auto-tracking
+});
+```
+
+Each route change will track:
+
+- `page_title`: Document title
+- `page_path`: Route path
+- `page_name`: Route name
+- `page_search`: Query parameters (JSON stringified)
+
+## API Reference
+
+### ZoriPlugin Options
+
+```typescript
+{
+ config: {
+ publishableKey: string; // required
+ baseUrl?: string; // optional
+ comebackThreshold?: number; // optional
+ trackQuickSwitches?: boolean; // optional
+ },
+ router?: Router; // optional Vue Router instance
+ autoTrackPageViews?: boolean; // optional, default true
+}
+```
+
+### useZori()
+
+Returns an object with:
+
+- `isInitialized`: Readonly ref indicating if ZoriHQ is ready
+- `track(eventName, properties)`: Track custom events
+- `identify(userInfo)`: Identify users
+- `getVisitorId()`: Get the visitor ID
+- `getSessionId()`: Get the current session ID
+- `setConsent(preferences)`: Set GDPR consent preferences
+- `optOut()`: Opt out of tracking completely
+- `hasConsent()`: Check if user has given consent
+
+### usePageView(properties?)
+
+Auto-track page view on component mount. Accepts static properties or reactive refs.
+
+### useTrackEvent(eventName, properties?)
+
+Track event on component mount. Supports reactive refs for both event name and properties.
+
+### useIdentify(userInfo)
+
+Identify user on component mount. Supports reactive refs.
+
+## TypeScript Support
+
+This package includes full TypeScript support:
+
+```typescript
+import type {
+ ZoriConfig,
+ ZoriInstance,
+ ConsentPreferences,
+ UserInfo,
+} from '@zorihq/vue';
+```
+
+## Composition API
+
+All composables follow Vue 3 Composition API conventions and support:
+
+- Reactive refs
+- Computed properties
+- Automatic cleanup on unmount
+
+## Options API Support
+
+You can also use the plugin with Options API:
+
+```vue
+
+```
+
+## License
+
+MIT
diff --git a/vue/index.ts b/vue/index.ts
new file mode 100644
index 0000000..91a0d54
--- /dev/null
+++ b/vue/index.ts
@@ -0,0 +1,302 @@
+import {
+ ref,
+ readonly,
+ onMounted,
+ onUnmounted,
+ watch,
+ inject,
+ provide,
+ type App,
+ type Ref,
+ type InjectionKey,
+} from "vue";
+import type {
+ ZoriConfig,
+ ConsentPreferences,
+ UserInfo,
+ ZoriCoreAPI,
+} from "@zorihq/types";
+
+export type { ZoriConfig, ConsentPreferences, UserInfo } from "@zorihq/types";
+
+export interface ZoriInstance extends Omit {
+ isInitialized: Readonly[>;
+ getSessionId: () => string | null;
+}
+
+export const ZoriKey: InjectionKey = Symbol("zori");
+
+export interface ZoriPluginOptions {
+ config: ZoriConfig;
+ router?: any;
+ autoTrackPageViews?: boolean;
+}
+
+function createZoriInstance(config: ZoriConfig): ZoriInstance {
+ const isInitialized = ref(false);
+ let scriptLoaded = false;
+
+ const loadScript = () => {
+ if (scriptLoaded || typeof window === "undefined") return;
+
+ (window as any).ZoriHQ = (window as any).ZoriHQ || [];
+
+ const script = document.createElement("script");
+ script.src = "https://cdn.zorihq.com/script.min.js";
+ script.async = true;
+ script.setAttribute("data-key", config.publishableKey);
+
+ if (config.baseUrl) {
+ script.setAttribute("data-base-url", config.baseUrl);
+ }
+
+ if (config.comebackThreshold !== undefined) {
+ script.setAttribute(
+ "data-comeback-threshold",
+ config.comebackThreshold.toString(),
+ );
+ }
+
+ if (config.trackQuickSwitches !== undefined) {
+ script.setAttribute(
+ "data-track-quick-switches",
+ config.trackQuickSwitches.toString(),
+ );
+ }
+
+ script.onload = () => {
+ isInitialized.value = true;
+ };
+
+ document.head.appendChild(script);
+ scriptLoaded = true;
+ };
+
+ const track = async (
+ eventName: string,
+ properties?: Record,
+ ): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.track === "function") {
+ return await zori.track(eventName, properties);
+ } else {
+ zori.push(["track", eventName, properties]);
+ return true;
+ }
+ };
+
+ const identify = async (userInfo: UserInfo): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.identify === "function") {
+ return await zori.identify(userInfo);
+ } else {
+ zori.push(["identify", userInfo]);
+ return true;
+ }
+ };
+
+ const getVisitorId = async (): Promise => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return "";
+
+ if (typeof zori.getVisitorId === "function") {
+ return await zori.getVisitorId();
+ }
+
+ return new Promise((resolve) => {
+ zori.push(["getVisitorId", (id: string) => resolve(id)]);
+ });
+ };
+
+ const getSessionId = (): string | null => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.getSessionId !== "function") return null;
+ return zori.getSessionId();
+ };
+
+ const setConsent = (preferences: ConsentPreferences): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.setConsent === "function") {
+ return zori.setConsent(preferences);
+ } else {
+ zori.push(["setConsent", preferences]);
+ return true;
+ }
+ };
+
+ const optOut = (): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori) return false;
+
+ if (typeof zori.optOut === "function") {
+ return zori.optOut();
+ } else {
+ zori.push(["optOut"]);
+ return true;
+ }
+ };
+
+ const hasConsent = (): boolean => {
+ const zori = (window as any).ZoriHQ;
+ if (!zori || typeof zori.hasConsent !== "function") return true;
+ return zori.hasConsent();
+ };
+
+ if (typeof window !== "undefined") {
+ loadScript();
+ }
+
+ return {
+ isInitialized: readonly(isInitialized),
+ track,
+ identify,
+ getVisitorId,
+ getSessionId,
+ setConsent,
+ optOut,
+ hasConsent,
+ };
+}
+
+export const ZoriPlugin = {
+ install(app: App, options: ZoriPluginOptions) {
+ const zoriInstance = createZoriInstance(options.config);
+ app.provide(ZoriKey, zoriInstance);
+
+ if (options.router && options.autoTrackPageViews !== false) {
+ options.router.afterEach((to: any) => {
+ if (zoriInstance.isInitialized.value) {
+ zoriInstance.track("page_view", {
+ page_title: document.title,
+ page_path: to.path,
+ page_name: to.name,
+ page_search: to.query ? JSON.stringify(to.query) : "",
+ });
+ }
+ });
+ }
+ },
+};
+
+export function useZori(): ZoriInstance {
+ const zori = inject(ZoriKey);
+ if (!zori) {
+ throw new Error(
+ "useZori must be used within a component with ZoriPlugin installed",
+ );
+ }
+ return zori;
+}
+
+export function usePageView(
+ properties?: Ref> | Record,
+) {
+ const { track, isInitialized } = useZori();
+
+ onMounted(() => {
+ if (isInitialized.value) {
+ const props =
+ typeof properties === "object" && "value" in properties
+ ? properties.value
+ : properties;
+ track("page_view", {
+ page_title: document.title,
+ page_path: window.location.pathname,
+ page_search: window.location.search,
+ page_hash: window.location.hash,
+ ...props,
+ });
+ }
+ });
+
+ if (properties && typeof properties === "object" && "value" in properties) {
+ watch(
+ properties,
+ (newProps) => {
+ if (isInitialized.value) {
+ track("page_view", {
+ page_title: document.title,
+ page_path: window.location.pathname,
+ page_search: window.location.search,
+ page_hash: window.location.hash,
+ ...newProps,
+ });
+ }
+ },
+ { deep: true },
+ );
+ }
+}
+
+export function useTrackEvent(
+ eventName: string | Ref,
+ properties?: Ref> | Record,
+) {
+ const { track, isInitialized } = useZori();
+
+ onMounted(() => {
+ if (isInitialized.value) {
+ const name = typeof eventName === "string" ? eventName : eventName.value;
+ const props =
+ typeof properties === "object" && "value" in properties
+ ? properties.value
+ : properties;
+ track(name, props);
+ }
+ });
+
+ watch(
+ [
+ typeof eventName === "string" ? ref(eventName) : eventName,
+ typeof properties === "object" && "value" in properties
+ ? properties
+ : ref(properties),
+ ],
+ ([newName, newProps]) => {
+ if (isInitialized.value) {
+ track(newName as string, newProps as Record);
+ }
+ },
+ { deep: true },
+ );
+}
+
+export function useIdentify(userInfo: Ref | UserInfo) {
+ const { identify, isInitialized } = useZori();
+
+ onMounted(() => {
+ const info =
+ typeof userInfo === "object" && "value" in userInfo
+ ? userInfo.value
+ : userInfo;
+ if (isInitialized.value && info) {
+ identify(info);
+ }
+ });
+
+ if (userInfo && typeof userInfo === "object" && "value" in userInfo) {
+ watch(
+ userInfo,
+ (newInfo) => {
+ if (isInitialized.value && newInfo) {
+ identify(newInfo);
+ }
+ },
+ { deep: true },
+ );
+ }
+}
+
+export default {
+ ZoriPlugin,
+ useZori,
+ usePageView,
+ useTrackEvent,
+ useIdentify,
+};
diff --git a/vue/package.json b/vue/package.json
new file mode 100644
index 0000000..421401c
--- /dev/null
+++ b/vue/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@zorihq/vue",
+ "version": "1.0.0",
+ "description": "ZoriHQ Analytics for Vue.js",
+ "main": "index.ts",
+ "types": "index.ts",
+ "exports": {
+ ".": {
+ "import": "./index.ts",
+ "require": "./index.ts",
+ "types": "./index.ts"
+ }
+ },
+ "files": [
+ "index.ts",
+ "README.md",
+ "package.json"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "echo 'Publishing source files'",
+ "test": "echo 'Tests not yet configured'"
+ },
+ "keywords": [
+ "analytics",
+ "vue",
+ "vue3",
+ "tracking",
+ "zorihq"
+ ],
+ "author": "ZoriHQ",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": ">=3.0.0"
+ },
+ "dependencies": {
+ "@zorihq/types": "^1.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0",
+ "vue": "^3.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zorihq/script.git",
+ "directory": "vue"
+ }
+}
diff --git a/vue/pnpm-lock.yaml b/vue/pnpm-lock.yaml
new file mode 100644
index 0000000..2a620df
--- /dev/null
+++ b/vue/pnpm-lock.yaml
@@ -0,0 +1,223 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@zorihq/types':
+ specifier: ^1.0.0
+ version: 1.0.0
+ devDependencies:
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+ vue:
+ specifier: ^3.0.0
+ version: 3.5.24(typescript@5.9.3)
+
+packages:
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.5':
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.28.5':
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+ engines: {node: '>=6.9.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@vue/compiler-core@3.5.24':
+ resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==}
+
+ '@vue/compiler-dom@3.5.24':
+ resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==}
+
+ '@vue/compiler-sfc@3.5.24':
+ resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==}
+
+ '@vue/compiler-ssr@3.5.24':
+ resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==}
+
+ '@vue/reactivity@3.5.24':
+ resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==}
+
+ '@vue/runtime-core@3.5.24':
+ resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==}
+
+ '@vue/runtime-dom@3.5.24':
+ resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==}
+
+ '@vue/server-renderer@3.5.24':
+ resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==}
+ peerDependencies:
+ vue: 3.5.24
+
+ '@vue/shared@3.5.24':
+ resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==}
+
+ '@zorihq/types@1.0.0':
+ resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ vue@3.5.24:
+ resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+snapshots:
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/parser@7.28.5':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@babel/types@7.28.5':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@vue/compiler-core@3.5.24':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@vue/shared': 3.5.24
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.24':
+ dependencies:
+ '@vue/compiler-core': 3.5.24
+ '@vue/shared': 3.5.24
+
+ '@vue/compiler-sfc@3.5.24':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@vue/compiler-core': 3.5.24
+ '@vue/compiler-dom': 3.5.24
+ '@vue/compiler-ssr': 3.5.24
+ '@vue/shared': 3.5.24
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.6
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.24':
+ dependencies:
+ '@vue/compiler-dom': 3.5.24
+ '@vue/shared': 3.5.24
+
+ '@vue/reactivity@3.5.24':
+ dependencies:
+ '@vue/shared': 3.5.24
+
+ '@vue/runtime-core@3.5.24':
+ dependencies:
+ '@vue/reactivity': 3.5.24
+ '@vue/shared': 3.5.24
+
+ '@vue/runtime-dom@3.5.24':
+ dependencies:
+ '@vue/reactivity': 3.5.24
+ '@vue/runtime-core': 3.5.24
+ '@vue/shared': 3.5.24
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.24
+ '@vue/shared': 3.5.24
+ vue: 3.5.24(typescript@5.9.3)
+
+ '@vue/shared@3.5.24': {}
+
+ '@zorihq/types@1.0.0': {}
+
+ csstype@3.2.3: {}
+
+ entities@4.5.0: {}
+
+ estree-walker@2.0.2: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ nanoid@3.3.11: {}
+
+ picocolors@1.1.1: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ source-map-js@1.2.1: {}
+
+ typescript@5.9.3: {}
+
+ vue@3.5.24(typescript@5.9.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.24
+ '@vue/compiler-sfc': 3.5.24
+ '@vue/runtime-dom': 3.5.24
+ '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3))
+ '@vue/shared': 3.5.24
+ optionalDependencies:
+ typescript: 5.9.3
diff --git a/vue/tsconfig.json b/vue/tsconfig.json
new file mode 100644
index 0000000..3517570
--- /dev/null
+++ b/vue/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["index.ts"],
+ "exclude": ["node_modules", "dist"]
+}
]