A Svelte 5 component library for building animated trading cards with a visual creator interface.
Build professional trading cards by composing pre-built SVG components. Think of it like Photoshop layers for cards - stack backgrounds, borders, frames, text fields, and decorations to create unique designs.
Key capabilities:
shapeSourceFor a comprehensive step-by-step integration guide, see GETTING-STARTED.md.
| Package | Version | Purpose |
|---|---|---|
| Svelte | 5.x | UI framework (runes) |
| SvelteKit | 2.x | Library packaging |
| Tailwind CSS | 4.x | Styling |
| Zod | 4.x | Runtime validation |
| shadcn-svelte | - | Creator UI components |
| hover-tilt | - | 3D tilt effects for card display |
| sharp | - | Image processing (WebP→PNG for server export) |
| @resvg/resvg-js | 2.x | Server-side SVG→PNG rendering |
<script lang="ts">
import { CardCanvas, registerComponent, Group } from 'svelte-trading-cards';
import { GradientBackground, Image, Border, TextField, Icon } from 'svelte-trading-cards';
import type { CardTemplate, CardData } from 'svelte-trading-cards';
// Register components
registerComponent('Group', Group);
registerComponent('GradientBackground', GradientBackground);
registerComponent('Image', Image);
registerComponent('Border', Border);
registerComponent('TextField', TextField);
registerComponent('Icon', Icon);
const template: CardTemplate = {
name: 'my-card',
components: [
{ id: '1', type: 'GradientBackground', props: { colors: ['#1e293b', '#0f172a'] } },
{ id: '2', type: 'Border', props: { color: '#fbbf24', width: 8 } },
{
id: '3',
type: 'Group',
props: { x: 50, y: 400, width: 650, height: 80 },
children: [
{ id: '3a', type: 'TextField', props: {
dataField: 'title',
maxFontSize: 48,
minFontSize: 16,
alignment: 'center'
}}
]
}
]
};
const data: CardData = { title: 'My Card Title' };
</script>
<CardCanvas {template} {data} />
The CardCreator component is a full-featured template designer you can embed in your app:
<script lang="ts">
import { CardCreator } from 'svelte-trading-cards';
import type { CardTemplate } from 'svelte-trading-cards';
// Define your data shape
const datasets = {
players: {
id: 'players',
name: 'Player Cards',
dataFields: [
{ value: 'name', label: 'Player Name', type: 'text' },
{ value: 'team', label: 'Team', type: 'text' },
{ value: 'photo', label: 'Photo', type: 'image' },
{ value: 'rating', label: 'Rating', type: 'number' }
],
cards: [
{ name: 'John Doe', team: 'Red Team', photo: '/players/john.jpg', rating: 85 }
]
}
};
function handleSave(data: { template: CardTemplate; editorState: unknown; name: string }) {
// Save to your database
console.log('Template saved:', data);
}
</script>
<CardCreator
{datasets}
initialDataset="players"
onSave={handleSave}
/>
Features:
<script>
import { CardCanvas, downloadSVG, downloadPNGClient } from 'svelte-trading-cards';
let svgElement;
</script>
<CardCanvas bind:svgElement {template} {data} />
<button onclick={() => downloadSVG(svgElement)}>Download SVG</button>
<button onclick={() => downloadPNGClient(svgElement)}>Download PNG</button>
Note: Client-side PNG export automatically embeds external images as base64 before rendering to canvas, avoiding CORS issues.
// SVG with 3mm bleed
downloadSVG(svgElement, { filename: 'my-card', bleedMm: 3 });
// PNG at 2x resolution with 3mm bleed
downloadPNGClient(svgElement, { filename: 'my-card', bleedMm: 3, scale: 2 });
The visual creator includes an Export button that opens a dialog with:
The library includes 37+ web-safe fonts and 40+ Google Fonts organized by category, plus dataset-specific brand fonts.
Web-Safe Fonts (no loading required):
| Category | Fonts | Examples |
|---|---|---|
| Sans-Serif | 13 | Arial, Helvetica, Verdana, Segoe UI, Futura |
| Serif | 10 | Georgia, Times New Roman, Palatino, Garamond |
| Monospace | 5 | Courier New, Consolas, Monaco |
| Display | 4 | Impact, Arial Black, Copperplate |
| Cursive | 5 | Brush Script, Lucida Handwriting, Comic Sans |
Google Fonts (loaded on demand):
| Category | Fonts | Examples |
|---|---|---|
| Sans-Serif | 11 | Roboto, Open Sans, Montserrat, Poppins, Nunito |
| Serif | 4 | Playfair Display, Merriweather, Lora, Crimson Text |
| Display | 17 | Oswald, Bangers, Orbitron, Press Start 2P, Bebas Neue |
| Monospace | 3 | Source Code Pro, Fira Code, JetBrains Mono |
| Cursive | 4 | Pacifico, Dancing Script, Caveat, Satisfy |
Google Fonts are loaded on demand when selected in the creator:
import {
loadGoogleFont, // Load single font on demand
getGoogleFontsUrlForCard, // Generate URL for all fonts in a card
isWebSafeFont, // Check if font needs loading
isGoogleFont, // Check if it's a known Google Font
waitForFonts // Wait for fonts to be ready
} from 'svelte-trading-cards';
// Load font when user selects it
await loadGoogleFont('Roboto, sans-serif');
// Preload all fonts for a card configuration
const url = getGoogleFontsUrlForCard(cardConfig);
if (url) {
const link = document.createElement('link');
link.href = url;
link.rel = 'stylesheet';
document.head.appendChild(link);
await waitForFonts(['Roboto', 'Oswald']);
}
Each dataset can have brand-specific fonts that appear first in the dropdown:
import { getAllFontsForDataset, getBrandFontOptions } from 'svelte-trading-cards';
// Get all fonts for PlayStation dataset (brand fonts first)
const fonts = getAllFontsForDataset('playstation');
// Get only brand fonts
const brandFonts = getBrandFontOptions('playstation');
import {
getAllFontsForDataset, // Brand + web-safe fonts for a dataset
getFontsByGroupForDataset, // Fonts organized by category
getWebSafeFonts, // Just web-safe fonts
getGoogleFontOptions, // Google Fonts for dropdowns
WEB_SAFE_FONTS, // Full font list with metadata
GOOGLE_FONTS, // Full Google Fonts list
FONT_GROUP_LABELS // Display names for categories
} from 'svelte-trading-cards';
Text automatically scales between min and max sizes to fit the container. All text-rendering components (TextField, StatPanel, List, Badge, Ribbon) use the same FitText algorithm ensuring text never overflows.
{
type: 'TextField',
props: {
dataField: 'title', // Bind to data field
maxFontSize: 48, // Maximum size
minFontSize: 12, // Minimum (scales to fit)
fontFamily: 'Arial',
fontWeight: 'bold',
color: '#ffffff',
alignment: 'center', // left, center, right
verticalAlign: 'center' // top, center, bottom
}
}
{
type: 'Border',
props: {
color: '#fbbf24',
width: 8,
layers: 3,
layerColors: ['#gold', '#silver', '#bronze'],
// Use standard effect system for glow (strokeGlow for border blur)
effect: { type: 'strokeGlow', blur: 10, intensity: 0.5, animated: true },
// Standard holographic config
holographic: {
color: '#fbbf24',
secondaryColor: '#06b6d4',
speed: 3,
apply: 'stroke'
}
}
}
Icons via Iconify API - 31 curated sets, all free for commercial use, no attribution required:
{
type: 'Icon',
props: {
iconData: { body: '<path d="M12 2L15.09..." fill="currentColor"/>', width: 24, height: 24 },
iconName: 'mdi:star', // Reference only
color: '#fbbf24',
size: 64, // Auto-fits container if omitted
rotation: 0, // 0-360 degrees
flipHorizontal: false,
flipVertical: false,
opacity: 1,
animation: { // Optional animation
type: 'spin',
speed: 'normal',
direction: 'clockwise'
}
}
}
Available icon sets: Material Design, Fluent UI, Phosphor, Tabler, Lucide, Simple Icons (brands), Noto Emoji, Circle Flags, Crypto Icons, and 22 more.
Display ratings with icons - supports 10 presets plus custom Iconify icons:
{
type: 'IconRating',
props: {
dataField: 'userRating', // Bind to data field
value: 4.5, // Or static value
max: 5, // Number of icons
sourceMax: 100, // Optional: scale values (47/100 → 2.35/5)
iconPreset: 'star', // star, heart, fire, thumbs-up, lightning,
// trophy, diamond, circle, pepper, skull, custom
filledColor: '#fbbf24',
emptyColor: '#374151',
size: 24,
gap: 4,
allowHalf: true, // Half-filled icons (★★★☆☆ for 3.5)
showValue: true, // Display value text
valueFormat: 'decimal' // decimal (4.5), fraction (4.5/5), percent (90%)
}
}
Use cases: User ratings, difficulty levels, spiciness meters, health bars, achievement tiers.
Repeating patterns for backgrounds with 17 geometric options plus custom icon patterns:
{
type: 'PatternBackground',
props: {
pattern: 'hexagons', // 17 geometric: dots, grid, diagonal, hexagons,
// triangles, squares, diamonds, chevrons, waves,
// circles, crosses, zigzag, checkered, stripes-h,
// stripes-v, confetti, stars
// Icon: 'icon' (single), 'icons' (multiple)
color: '#ffffff',
opacity: 0.1,
size: 32,
spacing: 0, // Gap between elements
rotation: 0, // Rotate entire pattern
strokeWidth: 1, // Line thickness
// For 'icons' pattern - multiple icons in sequence
icons: [
{ iconData: {...}, iconName: 'mdi:star', rotation: 0 },
{ iconData: {...}, iconName: 'mdi:heart', rotation: 15 }
],
rowOffset: 25 // Stagger rows for brick effect
}
}
Multi-icon pattern example: With icons [★, ♥, ◆] and rowOffset of 25:
★ ♥ ◆ ★ ♥ ◆
★ ♥ ◆ ★ ♥ ◆ (offset 25px)
★ ♥ ◆ ★ ♥ ◆
All components support CSS animations that are embedded in the SVG for portability. Animations work in preview and SVG exports, but are automatically static in PNG exports.
{
animation: {
type: 'spin', // spin, pulse, bounce, shake, float, glow, ping, trace
speed: 'normal', // slow (3s), normal (1.5s), fast (0.75s)
direction: 'clockwise', // clockwise, counterclockwise (spin & trace)
easing: 'ease-in-out', // linear, ease, ease-in, ease-out, ease-in-out
delay: 0, // Delay in seconds
iterationCount: 'infinite', // Number or 'infinite'
paused: false // Pause the animation
}
}
| Type | Description |
|---|---|
spin |
Continuous rotation (supports direction) |
pulse |
Scale up and down |
bounce |
Vertical bouncing |
shake |
Horizontal shake |
float |
Gentle floating motion |
glow |
Pulsing opacity |
ping |
Attention-grabbing scale + fade |
trace |
Neon sign drawing effect (best for borders) |
The trace animation creates a neon sign drawing effect:
All components support SVG filter-based effects that can be combined with animations.
{
effect: {
type: 'glow', // glow, strokeGlow, shadow, neon, innerGlow, lift, outline
color: '#3b82f6',
blur: 10,
intensity: 0.7,
animated: true, // Optional: enable pulsing
speed: 'normal' // slow, normal, fast
}
}
| Type | Description | Controls |
|---|---|---|
glow |
Soft outer glow (drop shadow style) | color, blur, intensity |
strokeGlow |
Blur glow on strokes/borders | color (optional), blur, intensity |
shadow |
Drop shadow | color, blur, offsetX, offsetY |
neon |
Neon sign effect (overrides color) | color, intensity, spread |
innerGlow |
Inward glow | color, blur, intensity |
lift |
Paper elevation shadow | elevation (sm/md/lg/xl) |
outline |
Stroke outline | color, width |
glow - Creates a drop shadow around the entire elementstrokeGlow - Creates a blur glow specifically on strokes/borders (uses element's stroke color if no color specified)The neon effect creates an intense neon sign look:
All visual components support both animation and effect props:
Layer blend modes enable Photoshop-like compositing effects. Applied via CSS mix-blend-mode on Groups.
| Mode | Category | Description |
|---|---|---|
normal |
Basic | No blending effect (default) |
multiply |
Darken | Darkens layers together - great for textures |
screen |
Lighten | Lightens layers together - great for glows |
overlay |
Contrast | Boosts contrast - multiply + screen combined |
darken |
Darken | Keeps the darker pixels |
lighten |
Lighten | Keeps the lighter pixels |
color-dodge |
Lighten | Intense brightening effect |
color-burn |
Darken | Intense darkening effect |
soft-light |
Contrast | Subtle contrast adjustment |
hard-light |
Contrast | Intense contrast effect |
difference |
Inversion | Creates color inversions |
exclusion |
Inversion | Softer inversion effect |
{
type: 'Group',
props: {
x: 100, y: 100,
width: 200, height: 200,
blendMode: 'multiply' // Apply blend mode to this layer
},
children: [...]
}
{
type: 'Group',
props: {
x: 100, y: 100,
width: 200, height: 200,
shapeSource: { type: 'builtin', shape: 'circle' }, // 22 built-in shapes
// Or custom: { type: 'custom', iconData: {...}, iconName: 'mdi:heart' }
clipContent: true
},
children: [...]
}
| Property | Value |
|---|---|
| Width | 750px |
| Height | 1050px |
| Corner Radius | 26px |
| Physical | 2.5" x 3.5" at 300 DPI |
For professional printing, cards support bleed areas:
| Property | Value |
|---|---|
| Max Bleed | 3mm (35px) |
| Bleed Width | 820px |
| Bleed Height | 1120px |
The Card Base layer automatically covers the bleed area. When you add a background image, it fills the bleed from the start - no manual adjustment needed.
npm install
npm run dev # Start dev server (localhost:5173)
npm run check # Type check
npm run build # Build library
/ - Demo gallery/creator - Visual template creator/test/text-fitting - Text fitting test pageimport { renderToSVGString, embedImages, svgToPNG } from 'svelte-trading-cards/server';
const svg = renderToSVGString(template, data);
const svgWithImages = await embedImages(svg); // Embeds external images as base64, converts webp→png
const { buffer } = await svgToPNG(svgWithImages);
Note: Server-side export uses sharp to convert WebP images to PNG (resvg-js doesn't support WebP). This is handled automatically by embedImages().
Server-side PNG conversion includes built-in protection against oversized or malicious SVG inputs:
import { svgToPNG, SVGValidationError } from 'svelte-trading-cards/server';
try {
const { buffer } = await svgToPNG(svg);
} catch (error) {
if (error instanceof SVGValidationError) {
// SVG exceeds size (5MB) or complexity (1000 groups) limits
console.error('Invalid SVG:', error.message);
}
}
// Skip validation for trusted sources
const { buffer } = await svgToPNG(trustedSvg, { skipValidation: true });
By default, components are registered globally. For multiple independent card instances, use isolated registries:
<script lang="ts">
import {
CardCanvas,
createComponentRegistry,
setComponentRegistry,
Group,
GradientBackground
} from 'svelte-trading-cards';
// Create isolated registry for this component tree
const registry = createComponentRegistry();
registry.register('Group', Group);
registry.register('GradientBackground', GradientBackground);
// Set in context (children will use this registry)
setComponentRegistry(registry);
</script>
<CardCanvas {template} {data} />
Download utilities automatically sanitize filenames to prevent path traversal:
import { downloadSVG, sanitizeFilename } from 'svelte-trading-cards';
// Automatically sanitized
downloadSVG(svg, { filename: '../../../etc/passwd' }); // Downloads as "etcpasswd.svg"
// Manual sanitization
const safe = sanitizeFilename(userInput); // "My Card!" → "My Card"
The display module provides interactive cards with 3D tilt effects, rarity-based visual presets, and flip animation support.
<script lang="ts">
import { Card } from 'svelte-trading-cards/display';
import type { CardTemplate, CardData } from 'svelte-trading-cards';
const template: CardTemplate = { /* your template */ };
const data: CardData = { /* your data */ };
</script>
<Card {template} {data} rarity="rare" />
Create two-sided cards with smooth 3D flip animations:
<script lang="ts">
import { Card } from 'svelte-trading-cards/display';
// Front and back templates
const frontTemplate: CardTemplate = { /* front design */ };
const backTemplate: CardTemplate = { /* back design */ };
const cardData: CardData = { /* shared or separate data */ };
// Track flip state (bindable)
let flipped = $state(false);
</script>
<!-- Click to flip -->
<Card
template={frontTemplate}
backTemplate={backTemplate}
data={cardData}
bind:flipped
flipOnClick
flipDuration={600}
rarity="legendary"
/>
<!-- Or flip on hover -->
<Card
template={frontTemplate}
backTemplate={backTemplate}
data={cardData}
flipOnHover
/>
<!-- Programmatic flip control -->
<button onclick={() => flipped = !flipped}>Flip Card</button>
| Prop | Type | Default | Description |
|---|---|---|---|
template |
CardTemplate |
required | Front face template |
backTemplate |
CardTemplate |
- | Back face template (enables flip) |
data |
CardData |
{} |
Data for front template |
backData |
CardData |
- | Data for back (defaults to data) |
flipped |
boolean |
false |
Current flip state (bindable) |
flipOnClick |
boolean |
false |
Flip when clicked |
flipOnHover |
boolean |
false |
Flip on mouse enter/leave |
flipDuration |
number |
600 |
Animation duration in ms |
rarity |
Rarity |
'common' |
Visual effect preset |
disabled |
boolean |
false |
Disable tilt effects (static) |
Each rarity level has unique visual effects:
| Rarity | Glare | Effect | Shadow |
|---|---|---|---|
common |
15% | none | subtle |
uncommon |
25% | none | medium |
rare |
40% | foil stripes | strong |
epic |
60% | holo bands | intense |
legendary |
80% | prism angular | maximum |
mythic |
90% | rainbow | maximum |
Define custom glare effects in your template:
const template: CardTemplate = {
name: 'Custom Card',
display: {
rarity: 'rare',
customGradient: 'linear-gradient(135deg, #ff6b6b, #4ecdc4, #45b7d1)'
},
components: [...]
};
| Domain | Front | Back |
|---|---|---|
| Trading cards | Character art, stats | Lore, abilities, flavor text |
| Employee badges | Photo, name, title | Emergency contacts, QR code |
| Product cards | Product image, price | Specifications, barcode |
| Event tickets | Event details, seat | QR code, terms |
| Business cards | Name, contact info | Services, social links |
| Achievement cards | Trophy icon, game | Description, unlock date |
Data adapters provide a structured way to transform domain-specific data into the CardData format used by templates. They document available fields and provide sample data for previews.
import {
PlayStationAdapter,
XboxAdapter,
SteamAdapter,
adapterRegistry
} from 'svelte-trading-cards/adapters';
// Transform PlayStation trophy data
const psnTrophy = {
gameTitle: 'Elden Ring',
trophyName: 'Elden Lord',
trophyDescription: 'Achieved all endings',
trophyIconUrl: 'https://example.com/trophy.png',
trophyType: 'platinum',
psnTrophyRarity: 'Ultra Rare',
earnedDate: '2024-01-15'
};
const cardData = PlayStationAdapter.transform(psnTrophy);
// { title: 'Elden Ring', subtitle: 'Elden Lord', ... }
// Get available fields for the creator
const fields = PlayStationAdapter.getFields();
// [{ key: 'title', label: 'Game Title', type: 'string' }, ...]
// Get sample data for previews
const sample = PlayStationAdapter.getSampleData();
import type { DataAdapter } from 'svelte-trading-cards/adapters';
interface Employee {
firstName: string;
lastName: string;
title: string;
department: string;
photoUrl: string;
employeeId: string;
}
const EmployeeAdapter: DataAdapter<Employee> = {
id: 'employee',
name: 'Employee Badge',
description: 'Transform HR employee data into badge format',
transform(employee) {
return {
title: `${employee.firstName} ${employee.lastName}`,
subtitle: employee.title,
description: employee.department,
imageUrl: employee.photoUrl,
employeeId: employee.employeeId
};
},
getFields() {
return [
{ key: 'title', label: 'Full Name', type: 'string', required: true },
{ key: 'subtitle', label: 'Job Title', type: 'string', required: true },
{ key: 'description', label: 'Department', type: 'string' },
{ key: 'imageUrl', label: 'Photo', type: 'image', required: true },
{ key: 'employeeId', label: 'Employee ID', type: 'string' }
];
},
getSampleData() {
return {
title: 'Jane Smith',
subtitle: 'Senior Engineer',
description: 'Engineering',
imageUrl: 'https://example.com/photo.jpg',
employeeId: 'EMP-12345'
};
}
};
// Register the adapter
import { adapterRegistry } from 'svelte-trading-cards/adapters';
adapterRegistry.register(EmployeeAdapter);
import { adapterRegistry } from 'svelte-trading-cards/adapters';
// Get all registered adapters
const allAdapters = adapterRegistry.getAll();
// Get a specific adapter by ID
const psAdapter = adapterRegistry.get('playstation');
// Get fields for an adapter
const fields = adapterRegistry.getFieldsForAdapter('playstation');
// Check if an adapter exists
if (adapterRegistry.has('custom')) {
// ...
}
interface DataAdapter<TSource = unknown> {
/** Unique identifier for this adapter */
id: string;
/** Human-readable name */
name: string;
/** Description of what data this adapter handles */
description?: string;
/** Transform source data to CardData */
transform(source: TSource): CardData;
/** Available fields this adapter provides */
getFields(): DataFieldDefinition[];
/** Generate sample data for preview */
getSampleData(): CardData;
/** Optional: Validate source data before transform */
validate?(source: unknown): source is TSource;
/** Optional: Suggested template IDs for this data type */
suggestedTemplates?: string[];
}
interface DataFieldDefinition {
/** Field key in CardData */
key: string;
/** Human-readable label */
label: string;
/** Data type */
type: 'string' | 'number' | 'date' | 'image' | 'array' | 'boolean';
/** Description for documentation */
description?: string;
/** Example value for preview/testing */
example?: unknown;
/** Whether this field is required */
required?: boolean;
}
Generate optimized preview images for social media sharing (Twitter, Facebook, Discord, etc.).
import { renderOGImage } from 'svelte-trading-cards/server';
const { buffer, width, height } = await renderOGImage(template, data, {
preset: 'twitter',
background: '#1a1a2e',
branding: {
logo: { url: 'https://yourapp.com/logo.png', position: 'top-left' },
watermark: { text: 'yourapp.com', position: 'bottom-right' }
}
});
| Preset | Dimensions | Platform |
|---|---|---|
twitter |
1200x628 | Twitter/X |
facebook |
1200x630 | |
discord |
1200x675 | Discord |
linkedin |
1200x627 | |
square |
1200x1200 | |
portrait |
900x1200 | Taller layouts |
await renderOGImage(template, data, {
preset: 'twitter', // or custom size: { width: 1200, height: 630 }
background: '#0f172a', // solid color
backgroundGradient: { // or gradient
from: '#1e1b4b',
to: '#0f172a',
direction: 'diagonal' // vertical, horizontal, diagonal
},
cardScale: 0.85, // how much of image height the card takes
cardPosition: 'center', // left, center, right
scale: 2, // output resolution multiplier
branding: {
logo: {
url: 'https://yourapp.com/logo.png',
position: 'top-left', // top-left, top-right, bottom-left, etc.
size: 48,
padding: 24,
opacity: 1
},
watermark: {
text: 'yourapp.com',
position: 'bottom-right',
color: '#ffffff',
opacity: 0.6,
fontSize: 18
},
caption: {
title: 'Card Title',
subtitle: 'by @username',
position: 'below' // below or right
}
}
});
// src/routes/api/og/[id].png/+server.ts
import { renderOGImage } from 'svelte-trading-cards/server';
export async function GET({ params }) {
const card = await db.cards.findById(params.id);
const template = await db.templates.findById(card.templateId);
const { buffer } = await renderOGImage(template, card.data, {
preset: 'twitter',
background: '#0f172a',
branding: {
logo: { url: 'https://yourapp.com/logo.png' },
watermark: { text: 'yourapp.com' }
}
});
return new Response(buffer, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
}
});
}
Then add OG meta tags to your card page:
<svelte:head>
<meta property="og:image" content="https://yourapp.com/api/og/{cardId}.png" />
<meta property="og:title" content={card.title} />
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
yourapp.com/cards/abc123og:image meta tagCaching: Facebook caches ~30 days, Twitter ~7 days, Discord ~24 hours. Your server only gets hit once per card per platform.
| Import Path | Contents |
|---|---|
svelte-trading-cards |
All components, CardCreator, types, client utilities |
svelte-trading-cards/creator |
CardCreator component and creator types only |
svelte-trading-cards/display |
Interactive Card with hover-tilt effects and rarity presets |
svelte-trading-cards/gallery |
CardRow and gallery layout components |
svelte-trading-cards/server |
Server-side rendering, image embedding, PNG conversion, OG images |
svelte-trading-cards/adapters |
Data adapters and adapter registry |
MIT