Skip to content

Svelte Integration

This guide shows how to integrate the Glyph editor into Svelte and SvelteKit applications with full TypeScript support and reactive stores.

Terminal window
npm install @glyph-sdk/web

Since Svelte has excellent support for web components, you simply import the SDK and it registers automatically:

src/lib/glyph.ts
import '@glyph-sdk/web';
// Re-export for convenience
export { };

Create type declarations for the web component:

src/lib/types/glyph.d.ts
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"]
}
}

Since Glyph is a client-side web component, use SvelteKit’s browser check:

src/lib/components/GlyphEditor.svelte
<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>
src/routes/quotes/+page.svelte
<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>

Create a reactive store for managing quote state:

src/lib/stores/quote.ts
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 data
export 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 store
function 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();
src/routes/quotes/builder/+page.svelte
<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>

For secure PDF generation, create a SvelteKit server endpoint:

src/routes/api/generate-pdf/+server.ts
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>

Set up your environment variables:

.env
PUBLIC_GLYPH_API_KEY=gk_your_public_key
GLYPH_API_KEY=gk_your_secret_key

If you’re using Svelte 5 with runes, here’s how to adapt the component:

src/lib/components/GlyphEditorRunes.svelte
<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}

<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}
/>

Wrap the editor in Svelte’s error handling:

src/lib/components/GlyphErrorBoundary.svelte
<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>