Svelte Integration
This guide shows how to integrate the Glyph editor into Svelte and SvelteKit applications with full TypeScript support and reactive stores.
Installation
Section titled “Installation”npm install @glyph-sdk/webyarn add @glyph-sdk/webpnpm add @glyph-sdk/webBasic Setup
Section titled “Basic Setup”1. Register the Web Component
Section titled “1. Register the Web Component”Since Svelte has excellent support for web components, you simply import the SDK and it registers automatically:
import '@glyph-sdk/web';
// Re-export for convenienceexport { };2. Add TypeScript Declarations
Section titled “2. Add TypeScript Declarations”Create type declarations for the web component:
declare namespace svelteHTML { interface IntrinsicElements { 'glyph-editor': { 'api-key'?: string; template?: string; data?: string; 'base-url'?: string; theme?: 'light' | 'dark' | 'system'; class?: string; }; }}
interface GlyphEditorElement extends HTMLElement { setData(data: object): void; setTemplate(templateId: string): void; getSessionId(): string | null; getHtml(): string; modify(prompt: string, options?: { region?: string }): Promise<void>; generatePdf(options?: object): Promise<Blob>; generatePng(options?: object): Promise<Blob>; undo(): void; redo(): void;}
interface GlyphReadyEvent extends CustomEvent<void> {}interface GlyphModifiedEvent extends CustomEvent<{ html: string; prompt: string }> {}interface GlyphSavedEvent extends CustomEvent<{ sessionId: string; format: string }> {}interface GlyphErrorEvent extends CustomEvent<{ code: string; message: string }> {}interface GlyphRegionSelectedEvent extends CustomEvent<{ region: string; element: HTMLElement }> {}Add the types to your tsconfig.json:
{ "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "types": ["./src/lib/types/glyph.d.ts"] }}SvelteKit Integration
Section titled “SvelteKit Integration”1. Create a Client-Side Wrapper Component
Section titled “1. Create a Client-Side Wrapper Component”Since Glyph is a client-side web component, use SvelteKit’s browser check:
<script lang="ts"> import { onMount, onDestroy, createEventDispatcher } from 'svelte'; import { browser } from '$app/environment';
// Props export let apiKey: string; export let template: string = 'quote-modern'; export let data: object = {}; export let baseUrl: string | undefined = undefined; export let theme: 'light' | 'dark' | 'system' = 'system';
// Event dispatcher const dispatch = createEventDispatcher<{ ready: void; modified: { html: string; prompt: string }; saved: { sessionId: string }; error: { code: string; message: string }; regionSelected: { region: string; element: HTMLElement }; }>();
// Editor reference let editorElement: GlyphEditorElement; let isReady = false;
// Import SDK on client-side only onMount(async () => { if (browser) { await import('@glyph-sdk/web'); } });
// Event handlers function handleReady() { isReady = true; dispatch('ready'); }
function handleModified(event: GlyphModifiedEvent) { dispatch('modified', event.detail); }
function handleSaved(event: GlyphSavedEvent) { dispatch('saved', event.detail); }
function handleError(event: GlyphErrorEvent) { dispatch('error', event.detail); }
function handleRegionSelected(event: GlyphRegionSelectedEvent) { dispatch('regionSelected', event.detail); }
// Attach event listeners onMount(() => { if (!editorElement) return;
editorElement.addEventListener('glyph:ready', handleReady); editorElement.addEventListener('glyph:modified', handleModified as EventListener); editorElement.addEventListener('glyph:saved', handleSaved as EventListener); editorElement.addEventListener('glyph:error', handleError as EventListener); editorElement.addEventListener('glyph:region-selected', handleRegionSelected as EventListener);
return () => { editorElement.removeEventListener('glyph:ready', handleReady); editorElement.removeEventListener('glyph:modified', handleModified as EventListener); editorElement.removeEventListener('glyph:saved', handleSaved as EventListener); editorElement.removeEventListener('glyph:error', handleError as EventListener); editorElement.removeEventListener('glyph:region-selected', handleRegionSelected as EventListener); }; });
// Reactive data update $: if (isReady && editorElement && data) { editorElement.setData(data); }
// Exposed methods export async function modify(prompt: string, region?: string): Promise<void> { return editorElement?.modify(prompt, region ? { region } : undefined); }
export async function generatePdf(options?: object): Promise<Blob | null> { return editorElement?.generatePdf(options) ?? null; }
export async function generatePng(options?: object): Promise<Blob | null> { return editorElement?.generatePng(options) ?? null; }
export function getSessionId(): string | null { return editorElement?.getSessionId() ?? null; }
export function getHtml(): string { return editorElement?.getHtml() ?? ''; }
export function undo(): void { editorElement?.undo(); }
export function redo(): void { editorElement?.redo(); }
export function setTemplate(templateId: string): void { editorElement?.setTemplate(templateId); }</script>
{#if browser} <glyph-editor bind:this={editorElement} api-key={apiKey} {template} data={JSON.stringify(data)} base-url={baseUrl} {theme} class={$$props.class} />{:else} <div class="glyph-placeholder"> <slot name="loading"> <p>Loading editor...</p> </slot> </div>{/if}
<style> glyph-editor { display: block; height: 100%; min-height: 500px; }
.glyph-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; min-height: 500px; background: #f8fafc; border-radius: 0.5rem; color: #64748b; }</style>2. Use in a Page
Section titled “2. Use in a Page”<script lang="ts"> import { PUBLIC_GLYPH_API_KEY } from '$env/static/public'; import GlyphEditor from '$lib/components/GlyphEditor.svelte';
let editor: GlyphEditor; let prompt = ''; let isModifying = false; let error: string | null = null;
const quoteData = { client: { name: 'Sarah Chen', company: 'Acme Technologies', email: 'sarah@acme.tech', }, lineItems: [ { description: 'Platform Development', details: 'Full-stack SaaS platform build', quantity: 1, unitPrice: 35000, total: 35000, }, { description: 'Integration Services', quantity: 2, unitPrice: 5000, total: 10000, }, ], totals: { subtotal: 45000, tax: 3600, total: 48600, }, meta: { quoteNumber: 'Q-2025-001', date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }), }, };
async function handleModify() { if (!prompt.trim()) return;
isModifying = true; error = null;
try { await editor.modify(prompt); prompt = ''; } catch (e) { error = e instanceof Error ? e.message : 'Modification failed'; } finally { isModifying = false; } }
async function handleDownload() { const pdf = await editor.generatePdf(); if (pdf) { const url = URL.createObjectURL(pdf); const a = document.createElement('a'); a.href = url; a.download = `quote-${quoteData.meta.quoteNumber}.pdf`; a.click(); URL.revokeObjectURL(url); } }</script>
<div class="quote-page"> <header> <h1>Create Quote</h1> </header>
<div class="toolbar"> <input type="text" bind:value={prompt} placeholder="Describe changes (e.g., 'Make the header navy blue')" disabled={isModifying} on:keydown={(e) => e.key === 'Enter' && handleModify()} /> <button on:click={handleModify} disabled={isModifying || !prompt.trim()}> {isModifying ? 'Applying...' : 'Apply'} </button> <button on:click={handleDownload}> Download PDF </button> </div>
{#if error} <div class="error">{error}</div> {/if}
<GlyphEditor bind:this={editor} apiKey={PUBLIC_GLYPH_API_KEY} template="quote-modern" data={quoteData} on:ready={() => console.log('Editor ready')} on:modified={(e) => console.log('Modified:', e.detail.prompt)} on:error={(e) => error = e.detail.message} /></div>
<style> .quote-page { display: flex; flex-direction: column; height: 100vh; padding: 1.5rem; }
header h1 { margin: 0 0 1rem; font-size: 1.5rem; }
.toolbar { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.toolbar input { flex: 1; padding: 0.5rem 1rem; border: 1px solid #e2e8f0; border-radius: 0.375rem; }
.toolbar button { padding: 0.5rem 1rem; background: #7c3aed; color: white; border: none; border-radius: 0.375rem; cursor: pointer; }
.toolbar button:disabled { opacity: 0.5; cursor: not-allowed; }
.error { padding: 0.75rem 1rem; background: #fef2f2; color: #dc2626; border-radius: 0.375rem; margin-bottom: 1rem; }</style>Svelte Stores for State Management
Section titled “Svelte Stores for State Management”Create a reactive store for managing quote state:
import { writable, derived } from 'svelte/store';
export interface LineItem { description: string; details?: string; quantity: number; unitPrice: number; total: number;}
export interface QuoteData { client: { name: string; company?: string; email?: string; address?: string; }; lineItems: LineItem[]; totals: { subtotal: number; discount?: number; tax?: number; total: number; }; meta?: { quoteNumber?: string; date?: string; validUntil?: string; };}
function createQuoteStore() { const { subscribe, set, update } = writable<QuoteData>({ client: { name: '' }, lineItems: [], totals: { subtotal: 0, total: 0 }, });
function recalculateTotals(data: QuoteData): QuoteData { const subtotal = data.lineItems.reduce((sum, item) => sum + item.total, 0); const discount = data.totals.discount || 0; const tax = (subtotal - discount) * 0.08; // 8% tax
return { ...data, totals: { ...data.totals, subtotal, tax, total: subtotal - discount + tax, }, }; }
return { subscribe, set,
setClient: (client: QuoteData['client']) => update((data) => ({ ...data, client })),
addLineItem: (item: Omit<LineItem, 'total'>) => update((data) => recalculateTotals({ ...data, lineItems: [ ...data.lineItems, { ...item, total: item.quantity * item.unitPrice }, ], }) ),
updateLineItem: (index: number, item: Partial<LineItem>) => update((data) => { const lineItems = [...data.lineItems]; lineItems[index] = { ...lineItems[index], ...item }; if (item.quantity !== undefined || item.unitPrice !== undefined) { lineItems[index].total = lineItems[index].quantity * lineItems[index].unitPrice; } return recalculateTotals({ ...data, lineItems }); }),
removeLineItem: (index: number) => update((data) => recalculateTotals({ ...data, lineItems: data.lineItems.filter((_, i) => i !== index), }) ),
setDiscount: (discount: number) => update((data) => recalculateTotals({ ...data, totals: { ...data.totals, discount }, }) ),
setMeta: (meta: QuoteData['meta']) => update((data) => ({ ...data, meta })),
reset: () => set({ client: { name: '' }, lineItems: [], totals: { subtotal: 0, total: 0 }, }), };}
export const quoteStore = createQuoteStore();
// Derived store for formatted dataexport const formattedTotals = derived(quoteStore, ($quote) => ({ subtotal: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format($quote.totals.subtotal), discount: $quote.totals.discount ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format($quote.totals.discount) : null, tax: $quote.totals.tax ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format($quote.totals.tax) : null, total: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format($quote.totals.total),}));
// Modification history storefunction createModificationHistory() { const { subscribe, update } = writable<Array<{ prompt: string; timestamp: Date }>>([]);
return { subscribe, add: (prompt: string) => update((history) => [...history, { prompt, timestamp: new Date() }]), clear: () => update(() => []), };}
export const modificationHistory = createModificationHistory();Using Stores with the Editor
Section titled “Using Stores with the Editor”<script lang="ts"> import { PUBLIC_GLYPH_API_KEY } from '$env/static/public'; import GlyphEditor from '$lib/components/GlyphEditor.svelte'; import { quoteStore, formattedTotals, modificationHistory } from '$lib/stores/quote';
let editor: GlyphEditor; let prompt = ''; let isModifying = false;
// Initialize with sample data quoteStore.setClient({ name: 'Alex Morgan', company: 'Startup Labs', email: 'alex@startuplabs.io', }); quoteStore.addLineItem({ description: 'MVP Development', details: 'Full-stack application', quantity: 1, unitPrice: 25000, });
async function handleModify() { if (!prompt.trim()) return;
isModifying = true; try { await editor.modify(prompt); modificationHistory.add(prompt); prompt = ''; } finally { isModifying = false; } }
function addItem() { quoteStore.addLineItem({ description: '', quantity: 1, unitPrice: 0, }); }
function removeItem(index: number) { quoteStore.removeLineItem(index); }</script>
<div class="builder"> <aside class="sidebar"> <section> <h2>Client</h2> <input type="text" placeholder="Name" value={$quoteStore.client.name} on:input={(e) => quoteStore.setClient({ ...$quoteStore.client, name: e.currentTarget.value })} /> <input type="text" placeholder="Company" value={$quoteStore.client.company || ''} on:input={(e) => quoteStore.setClient({ ...$quoteStore.client, company: e.currentTarget.value })} /> <input type="email" placeholder="Email" value={$quoteStore.client.email || ''} on:input={(e) => quoteStore.setClient({ ...$quoteStore.client, email: e.currentTarget.value })} /> </section>
<section> <h2>Line Items</h2> {#each $quoteStore.lineItems as item, index} <div class="line-item"> <input type="text" placeholder="Description" value={item.description} on:input={(e) => quoteStore.updateLineItem(index, { description: e.currentTarget.value })} /> <input type="number" placeholder="Qty" value={item.quantity} on:input={(e) => quoteStore.updateLineItem(index, { quantity: parseInt(e.currentTarget.value) || 1 })} /> <input type="number" placeholder="Price" value={item.unitPrice} on:input={(e) => quoteStore.updateLineItem(index, { unitPrice: parseFloat(e.currentTarget.value) || 0 })} /> <button class="remove" on:click={() => removeItem(index)}>x</button> </div> {/each} <button class="add-item" on:click={addItem}>+ Add Item</button> </section>
<section class="totals"> <div class="total-row"> <span>Subtotal</span> <span>{$formattedTotals.subtotal}</span> </div> {#if $formattedTotals.tax} <div class="total-row"> <span>Tax (8%)</span> <span>{$formattedTotals.tax}</span> </div> {/if} <div class="total-row final"> <span>Total</span> <span>{$formattedTotals.total}</span> </div> </section> </aside>
<main class="preview"> <div class="ai-bar"> <input type="text" bind:value={prompt} placeholder="Describe AI changes..." disabled={isModifying} on:keydown={(e) => e.key === 'Enter' && handleModify()} /> <button on:click={handleModify} disabled={isModifying || !prompt.trim()}> {isModifying ? 'Applying...' : 'Apply'} </button> </div>
<GlyphEditor bind:this={editor} apiKey={PUBLIC_GLYPH_API_KEY} template="quote-modern" data={$quoteStore} on:modified={(e) => modificationHistory.add(e.detail.prompt)} /> </main></div>
<style> .builder { display: grid; grid-template-columns: 350px 1fr; height: 100vh; }
.sidebar { padding: 1.5rem; background: #f8fafc; border-right: 1px solid #e2e8f0; overflow-y: auto; }
section { margin-bottom: 1.5rem; }
section h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin: 0 0 0.75rem; }
input { width: 100%; padding: 0.5rem; border: 1px solid #e2e8f0; border-radius: 0.375rem; margin-bottom: 0.5rem; }
.line-item { display: grid; grid-template-columns: 1fr 60px 80px 32px; gap: 0.5rem; margin-bottom: 0.5rem; }
.line-item input { margin-bottom: 0; }
.remove { padding: 0.5rem; background: #fee2e2; color: #dc2626; border: none; border-radius: 0.375rem; cursor: pointer; }
.add-item { width: 100%; padding: 0.5rem; background: #f1f5f9; border: 1px dashed #cbd5e1; border-radius: 0.375rem; cursor: pointer; color: #64748b; }
.totals { background: white; padding: 1rem; border-radius: 0.5rem; }
.total-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
.total-row.final { font-weight: bold; font-size: 1.125rem; border-top: 1px solid #e2e8f0; padding-top: 0.5rem; margin-top: 0.5rem; }
.preview { display: flex; flex-direction: column; padding: 1.5rem; overflow: hidden; }
.ai-bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.ai-bar input { flex: 1; margin-bottom: 0; }
.ai-bar button { padding: 0.5rem 1rem; background: #7c3aed; color: white; border: none; border-radius: 0.375rem; cursor: pointer; }
.ai-bar button:disabled { opacity: 0.5; cursor: not-allowed; }</style>Server-Side PDF Generation
Section titled “Server-Side PDF Generation”For secure PDF generation, create a SvelteKit server endpoint:
import { json } from '@sveltejs/kit';import { GLYPH_API_KEY } from '$env/static/private';import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => { try { const { html, sessionId } = await request.json();
if (!html && !sessionId) { return json({ error: 'Either html or sessionId is required' }, { status: 400 }); }
const response = await fetch('https://api.glyph.you/v1/generate', { method: 'POST', headers: { 'Authorization': `Bearer ${GLYPH_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ html, sessionId, format: 'pdf', }), });
if (!response.ok) { const error = await response.json(); return json(error, { status: response.status }); }
const pdfBuffer = await response.arrayBuffer();
return new Response(pdfBuffer, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="document.pdf"', }, }); } catch (error) { console.error('PDF generation error:', error); return json({ error: 'Failed to generate PDF' }, { status: 500 }); }};Use from your component:
<script lang="ts"> async function downloadViaServer() { const html = editor.getHtml(); if (!html) return;
const response = await fetch('/api/generate-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html }), });
if (!response.ok) { throw new Error('Failed to generate PDF'); }
const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'quote.pdf'; a.click(); URL.revokeObjectURL(url); }</script>Environment Variables
Section titled “Environment Variables”Set up your environment variables:
PUBLIC_GLYPH_API_KEY=gk_your_public_keyGLYPH_API_KEY=gk_your_secret_keySvelte 5 Runes (Preview)
Section titled “Svelte 5 Runes (Preview)”If you’re using Svelte 5 with runes, here’s how to adapt the component:
<script lang="ts"> import { onMount } from 'svelte'; import { browser } from '$app/environment';
interface Props { apiKey: string; template?: string; data?: object; baseUrl?: string; theme?: 'light' | 'dark' | 'system'; onready?: () => void; onmodified?: (detail: { html: string; prompt: string }) => void; onerror?: (detail: { code: string; message: string }) => void; }
let { apiKey, template = 'quote-modern', data = {}, baseUrl = undefined, theme = 'system', onready, onmodified, onerror, }: Props = $props();
let editorElement: GlyphEditorElement | undefined = $state(); let isReady = $state(false);
onMount(async () => { if (browser) { await import('@glyph-sdk/web'); } });
$effect(() => { if (!editorElement) return;
const handleReady = () => { isReady = true; onready?.(); };
const handleModified = (e: CustomEvent) => onmodified?.(e.detail); const handleError = (e: CustomEvent) => onerror?.(e.detail);
editorElement.addEventListener('glyph:ready', handleReady); editorElement.addEventListener('glyph:modified', handleModified as EventListener); editorElement.addEventListener('glyph:error', handleError as EventListener);
return () => { editorElement?.removeEventListener('glyph:ready', handleReady); editorElement?.removeEventListener('glyph:modified', handleModified as EventListener); editorElement?.removeEventListener('glyph:error', handleError as EventListener); }; });
// Reactive data sync $effect(() => { if (isReady && editorElement && data) { editorElement.setData(data); } });
// Exposed methods via module context export function modify(prompt: string, region?: string) { return editorElement?.modify(prompt, region ? { region } : undefined); }
export function generatePdf(options?: object) { return editorElement?.generatePdf(options); }
export function getHtml() { return editorElement?.getHtml() ?? ''; }</script>
{#if browser} <glyph-editor bind:this={editorElement} api-key={apiKey} {template} data={JSON.stringify(data)} base-url={baseUrl} {theme} />{:else} <div class="placeholder">Loading editor...</div>{/if}Styling with CSS Variables
Section titled “Styling with CSS Variables”<style> :global(glyph-editor) { --glyph-accent: #7c3aed; --glyph-accent-hover: #6d28d9; --glyph-radius: 0.5rem; --glyph-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); --glyph-font: 'Inter', system-ui, sans-serif; }
/* Dark mode */ :global(.dark glyph-editor) { --glyph-bg: #1e293b; --glyph-text: #f1f5f9; --glyph-border: #334155; }</style>Or use Tailwind CSS:
<GlyphEditor class="h-[700px] rounded-lg shadow-lg overflow-hidden [--glyph-accent:theme(colors.purple.600)]" {...props}/>Error Boundary
Section titled “Error Boundary”Wrap the editor in Svelte’s error handling:
<script lang="ts"> import { onMount } from 'svelte';
let hasError = false; let errorMessage = '';
onMount(() => { const handleError = (event: ErrorEvent) => { if (event.message.includes('glyph')) { hasError = true; errorMessage = event.message; event.preventDefault(); } };
window.addEventListener('error', handleError); return () => window.removeEventListener('error', handleError); });</script>
{#if hasError} <div class="error-fallback"> <h2>Document editor unavailable</h2> <p>Please refresh the page to try again.</p> <button on:click={() => location.reload()}>Refresh</button> </div>{:else} <slot />{/if}
<style> .error-fallback { padding: 2rem; background: #fef2f2; border-radius: 0.5rem; text-align: center; color: #dc2626; }
.error-fallback button { margin-top: 1rem; padding: 0.5rem 1rem; background: #dc2626; color: white; border: none; border-radius: 0.375rem; cursor: pointer; }</style>Usage:
<GlyphErrorBoundary> <GlyphEditor {...props} /></GlyphErrorBoundary>Next Steps
Section titled “Next Steps”- React Integration - React patterns with hooks
- Vue Integration - Vue 3 Composition API example
- Vanilla JS - Framework-agnostic usage
- SDK Reference - Complete API documentation
- Theming Guide - Customize the editor appearance