mirror of
https://github.com/LukeHagar/Sveltey.git
synced 2025-12-06 12:47:44 +00:00
adjusted name, updated lockfile, updated readme, adjusted styles, adjusted blog, testing glob
This commit is contained in:
411
README.md
411
README.md
@@ -1,9 +1,50 @@
|
|||||||
|
|
||||||
# 🚀 Sveltey - SvelteKit SaaS Template
|
# 🚀 Sveltey - SvelteKit SaaS Template
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supabase, and Skeleton UI. Get your SaaS project up and running in hours, not months.
|
A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supabase, and Skeleton UI. Get your SaaS project up and running in hours, not months.
|
||||||
|
|
||||||
|
- [🚀 Sveltey - SvelteKit SaaS Template](#-sveltey---sveltekit-saas-template)
|
||||||
|
- [✨ Features](#-features)
|
||||||
|
- [🔐 Authentication \& User Management](#-authentication--user-management)
|
||||||
|
- [🎨 Modern UI/UX](#-modern-uiux)
|
||||||
|
- [📝 Content Management](#-content-management)
|
||||||
|
- [📊 Dashboard \& Analytics - Planned](#-dashboard--analytics---planned)
|
||||||
|
- [💳 Payments \& Subscriptions - Planned](#-payments--subscriptions---planned)
|
||||||
|
- [🚀 Quick Start](#-quick-start)
|
||||||
|
- [📦 Tech Stack](#-tech-stack)
|
||||||
|
- [📁 Project Structure](#-project-structure)
|
||||||
|
- [🔧 Configuration](#-configuration)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Supabase Setup](#supabase-setup)
|
||||||
|
- [Stripe Setup](#stripe-setup)
|
||||||
|
- [🎯 Customization](#-customization)
|
||||||
|
- [Branding](#branding)
|
||||||
|
- [Components](#components)
|
||||||
|
- [🔍 SEO \& Meta Tags](#-seo--meta-tags)
|
||||||
|
- [Understanding the Meta Tag System](#understanding-the-meta-tag-system)
|
||||||
|
- [Base Meta Tags Structure](#base-meta-tags-structure)
|
||||||
|
- [Customizing Page Meta Tags](#customizing-page-meta-tags)
|
||||||
|
- [Dynamic Meta Tags](#dynamic-meta-tags)
|
||||||
|
- [Robot Control](#robot-control)
|
||||||
|
- [🖼️ OpenGraph Images](#️-opengraph-images)
|
||||||
|
- [Setting Up OpenGraph Images](#setting-up-opengraph-images)
|
||||||
|
- [1. Default Site Image](#1-default-site-image)
|
||||||
|
- [2. Page-Specific Images](#2-page-specific-images)
|
||||||
|
- [3. Dynamic Images for Blog Posts](#3-dynamic-images-for-blog-posts)
|
||||||
|
- [OpenGraph Image Best Practices](#opengraph-image-best-practices)
|
||||||
|
- [Image Specifications](#image-specifications)
|
||||||
|
- [Design Guidelines](#design-guidelines)
|
||||||
|
- [Dynamic Image Generation](#dynamic-image-generation)
|
||||||
|
- [Testing OpenGraph Images](#testing-opengraph-images)
|
||||||
|
- [Common OpenGraph Properties](#common-opengraph-properties)
|
||||||
|
- [🤝 Contributing](#-contributing)
|
||||||
|
- [📄 License](#-license)
|
||||||
|
- [🆘 Support](#-support)
|
||||||
|
- [🙏 Acknowledgments](#-acknowledgments)
|
||||||
|
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
### 🔐 Authentication & User Management
|
### 🔐 Authentication & User Management
|
||||||
@@ -20,21 +61,21 @@ A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supab
|
|||||||
- **Loading States**: Elegant loading indicators and transitions
|
- **Loading States**: Elegant loading indicators and transitions
|
||||||
- **Toast Notifications**: User-friendly feedback system
|
- **Toast Notifications**: User-friendly feedback system
|
||||||
|
|
||||||
### 📊 Dashboard & Analytics
|
### 📝 Content Management
|
||||||
|
- **Blog System**: Built-in blog with markdown support
|
||||||
|
- **SEO Optimized**: Meta tags, Open Graph, and structured data
|
||||||
|
|
||||||
|
### 📊 Dashboard & Analytics - Planned
|
||||||
- **Admin Dashboard**: Clean, intuitive admin interface
|
- **Admin Dashboard**: Clean, intuitive admin interface
|
||||||
- **User Analytics**: Basic user metrics and insights
|
- **User Analytics**: Basic user metrics and insights
|
||||||
- **Real-time Updates**: Live data updates using Supabase real-time
|
- **Real-time Updates**: Live data updates using Supabase real-time
|
||||||
- **Data Visualization**: Charts and graphs for key metrics
|
- **Data Visualization**: Charts and graphs for key metrics
|
||||||
|
|
||||||
### 📝 Content Management
|
### 💳 Payments & Subscriptions - Planned
|
||||||
- **Blog System**: Built-in blog with markdown support
|
|
||||||
- **SEO Optimized**: Meta tags, Open Graph, and structured data
|
|
||||||
|
|
||||||
<!-- ### 💳 Payments & Subscriptions
|
|
||||||
- **Stripe Integration**: Ready-to-use payment processing
|
- **Stripe Integration**: Ready-to-use payment processing
|
||||||
- **Subscription Plans**: Flexible pricing tiers
|
- **Subscription Plans**: Flexible pricing tiers
|
||||||
- **Billing Management**: Customer billing portal
|
- **Billing Management**: Customer billing portal
|
||||||
- **Webhook Handling**: Secure webhook processing -->
|
- **Webhook Handling**: Secure webhook processing
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -137,6 +178,362 @@ STRIPE_WEBHOOK_SECRET=your_webhook_secret
|
|||||||
- Skeleton UI provides the base component library
|
- Skeleton UI provides the base component library
|
||||||
- Easy to theme and customize with CSS variables
|
- Easy to theme and customize with CSS variables
|
||||||
|
|
||||||
|
## 🔍 SEO & Meta Tags
|
||||||
|
|
||||||
|
Sveltey comes with a comprehensive SEO system built on top of `svelte-meta-tags` that provides automatic meta tag management, OpenGraph support, and Twitter Card integration.
|
||||||
|
|
||||||
|
### Understanding the Meta Tag System
|
||||||
|
|
||||||
|
The meta tag system in Sveltey uses a two-level approach:
|
||||||
|
|
||||||
|
1. **Base Meta Tags** (`src/routes/+layout.ts`) - Global defaults for your entire site
|
||||||
|
2. **Page Meta Tags** (`src/routes/*/+page.ts`) - Page-specific overrides and additions
|
||||||
|
|
||||||
|
#### Base Meta Tags Structure
|
||||||
|
|
||||||
|
The base meta tags are defined in `src/routes/+layout.ts` and include:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const baseMetaTags = Object.freeze({
|
||||||
|
title: 'Sveltey - SvelteKit SaaS Template',
|
||||||
|
titleTemplate: '%s | Sveltey',
|
||||||
|
description: 'Your default site description...',
|
||||||
|
canonical: new URL(url.pathname, url.origin).href,
|
||||||
|
robots: 'index,follow',
|
||||||
|
keywords: ['SvelteKit', 'SaaS', 'template'],
|
||||||
|
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
url: new URL(url.pathname, url.origin).href,
|
||||||
|
title: 'Sveltey - SvelteKit SaaS Template',
|
||||||
|
description: 'Your OpenGraph description...',
|
||||||
|
siteName: 'Sveltey',
|
||||||
|
locale: 'en_US',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-image.jpg`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Sveltey - SvelteKit SaaS Template',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
twitter: {
|
||||||
|
cardType: 'summary_large_image',
|
||||||
|
site: '@sveltey_dev',
|
||||||
|
creator: '@sveltey_dev',
|
||||||
|
title: 'Sveltey - SvelteKit SaaS Template',
|
||||||
|
description: 'Your Twitter description...',
|
||||||
|
image: `${url.origin}/og-image.jpg`,
|
||||||
|
imageAlt: 'Sveltey - SvelteKit SaaS Template'
|
||||||
|
}
|
||||||
|
}) satisfies MetaTagsProps;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing Page Meta Tags
|
||||||
|
|
||||||
|
Each page can override and extend the base meta tags by exporting a `load` function in its `+page.ts` file:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/your-page/+page.ts
|
||||||
|
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
const pageMetaTags = Object.freeze({
|
||||||
|
title: 'Your Page Title',
|
||||||
|
description: 'Specific description for this page',
|
||||||
|
keywords: ['additional', 'keywords', 'for', 'this', 'page'],
|
||||||
|
|
||||||
|
openGraph: {
|
||||||
|
title: 'Your Page Title - Brand Name',
|
||||||
|
description: 'OpenGraph description for social sharing',
|
||||||
|
type: 'article', // or 'website', 'product', etc.
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://your-domain.com/specific-og-image.jpg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Description of your image'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
twitter: {
|
||||||
|
title: 'Twitter-specific title',
|
||||||
|
description: 'Twitter-specific description'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional meta tags
|
||||||
|
additionalMetaTags: [
|
||||||
|
{
|
||||||
|
name: 'author',
|
||||||
|
content: 'Your Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'article:published_time',
|
||||||
|
content: '2024-01-01T00:00:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) satisfies MetaTagsProps;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageMetaTags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Meta Tags
|
||||||
|
|
||||||
|
For dynamic pages (like blog posts), you can generate meta tags based on content:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/blog/[slug]/+page.server.ts
|
||||||
|
export const load = async ({ params, url }) => {
|
||||||
|
const post = await getPostBySlug(params.slug);
|
||||||
|
|
||||||
|
const pageMetaTags = Object.freeze({
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
canonical: new URL(`/blog/${params.slug}`, url.origin).href,
|
||||||
|
|
||||||
|
openGraph: {
|
||||||
|
type: 'article',
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
url: new URL(`/blog/${params.slug}`, url.origin).href,
|
||||||
|
images: post.featuredImage ? [
|
||||||
|
{
|
||||||
|
url: post.featuredImage,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: post.title
|
||||||
|
}
|
||||||
|
] : undefined,
|
||||||
|
article: {
|
||||||
|
publishedTime: post.publishedAt,
|
||||||
|
authors: [post.author],
|
||||||
|
section: 'Technology',
|
||||||
|
tags: post.tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) satisfies MetaTagsProps;
|
||||||
|
|
||||||
|
return { post, pageMetaTags };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Robot Control
|
||||||
|
|
||||||
|
Control search engine indexing per page:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pageMetaTags = {
|
||||||
|
robots: 'noindex,nofollow', // Don't index this page
|
||||||
|
// or
|
||||||
|
robots: 'index,follow', // Index this page (default)
|
||||||
|
// or
|
||||||
|
robots: 'index,nofollow' // Index but don't follow links
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🖼️ OpenGraph Images
|
||||||
|
|
||||||
|
OpenGraph images are crucial for social media sharing and SEO. Sveltey provides a flexible system for managing these images.
|
||||||
|
|
||||||
|
### Setting Up OpenGraph Images
|
||||||
|
|
||||||
|
#### 1. Default Site Image
|
||||||
|
|
||||||
|
Place your default OpenGraph image in the `static` folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
static/
|
||||||
|
├── og-image.jpg # Default 1200x630 image
|
||||||
|
├── og-image-square.jpg # Optional square variant
|
||||||
|
└── favicon.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The default image is automatically referenced in your base meta tags:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/+layout.ts
|
||||||
|
openGraph: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-image.jpg`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Sveltey - SvelteKit SaaS Template',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Page-Specific Images
|
||||||
|
|
||||||
|
Override the default image for specific pages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/pricing/+page.ts
|
||||||
|
const pageMetaTags = {
|
||||||
|
openGraph: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-pricing.jpg`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Sveltey Pricing Plans',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Dynamic Images for Blog Posts
|
||||||
|
|
||||||
|
For blog posts or dynamic content, you can generate or specify images dynamically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/blog/[slug]/+page.server.ts
|
||||||
|
const pageMetaTags = {
|
||||||
|
openGraph: {
|
||||||
|
images: post.featuredImage ? [
|
||||||
|
{
|
||||||
|
url: post.featuredImage,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: post.title,
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-blog-default.jpg`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Sveltey Blog',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenGraph Image Best Practices
|
||||||
|
|
||||||
|
#### Image Specifications
|
||||||
|
|
||||||
|
- **Recommended Size**: 1200x630 pixels (1.91:1 aspect ratio)
|
||||||
|
- **Minimum Size**: 600x315 pixels
|
||||||
|
- **Maximum Size**: 8MB
|
||||||
|
- **Format**: JPG or PNG (JPG preferred for smaller file size)
|
||||||
|
|
||||||
|
#### Design Guidelines
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example with multiple image variants
|
||||||
|
openGraph: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-image-large.jpg`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Large image for Facebook, LinkedIn',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${url.origin}/og-image-square.jpg`,
|
||||||
|
width: 1080,
|
||||||
|
height: 1080,
|
||||||
|
alt: 'Square image for Instagram, Twitter',
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dynamic Image Generation
|
||||||
|
|
||||||
|
For advanced use cases, you can generate images dynamically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/api/og/[slug]/+server.ts
|
||||||
|
export async function GET({ params, url }) {
|
||||||
|
const post = await getPostBySlug(params.slug);
|
||||||
|
|
||||||
|
// Generate image using libraries like @vercel/og or canvas
|
||||||
|
const image = await generateOGImage({
|
||||||
|
title: post.title,
|
||||||
|
author: post.author,
|
||||||
|
template: 'blog-post'
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(image, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in your meta tags:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
openGraph: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${url.origin}/api/og/${params.slug}`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: post.title,
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing OpenGraph Images
|
||||||
|
|
||||||
|
Use these tools to test your OpenGraph implementation:
|
||||||
|
|
||||||
|
- **Facebook Debugger**: https://developers.facebook.com/tools/debug/
|
||||||
|
- **Twitter Card Validator**: https://cards-dev.twitter.com/validator
|
||||||
|
- **LinkedIn Post Inspector**: https://www.linkedin.com/post-inspector/
|
||||||
|
|
||||||
|
### Common OpenGraph Properties
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
openGraph: {
|
||||||
|
type: 'website', // website, article, product, etc.
|
||||||
|
title: 'Page Title', // Specific title for social sharing
|
||||||
|
description: 'Description', // Social media description
|
||||||
|
url: 'https://example.com', // Canonical URL
|
||||||
|
siteName: 'Site Name', // Your site/brand name
|
||||||
|
locale: 'en_US', // Language and region
|
||||||
|
|
||||||
|
// For articles
|
||||||
|
article: {
|
||||||
|
publishedTime: '2024-01-01T00:00:00Z',
|
||||||
|
modifiedTime: '2024-01-02T00:00:00Z',
|
||||||
|
authors: ['Author Name'],
|
||||||
|
section: 'Technology',
|
||||||
|
tags: ['svelte', 'sveltekit']
|
||||||
|
},
|
||||||
|
|
||||||
|
// For products
|
||||||
|
product: {
|
||||||
|
price: {
|
||||||
|
amount: '29.99',
|
||||||
|
currency: 'USD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "saasy",
|
"name": "sveltey",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "saasy",
|
"name": "sveltey",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
|
||||||
import type { BlogPost } from '$lib/blog';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Calendar, User, Tag, Share2, ArrowLeft, Star, Twitter, Linkedin } from '@lucide/svelte';
|
import { Calendar, User, Tag, Share2, ArrowLeft, Star, Twitter, Linkedin } from '@lucide/svelte';
|
||||||
|
|
||||||
@@ -21,7 +19,7 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
// Dynamically import the markdown component
|
// Dynamically import the markdown component
|
||||||
const module = await import(`../../../lib/posts/${data.slug}.md`);
|
const module = import.meta.glob(`$lib/posts/${data.slug}.md`);
|
||||||
component = module.default;
|
component = module.default;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load blog post component:', err);
|
console.error('Failed to load blog post component:', err);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
const baseClasses = 'btn flex items-center gap-2';
|
const baseClasses = 'btn flex items-center gap-2';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return `${baseClasses} preset-filled-primary-500 cursor-default`;
|
return `${baseClasses} preset-filled-primary-500 text-on-primary-500 cursor-default`;
|
||||||
}
|
}
|
||||||
return `${baseClasses} preset-ghost-surface-200-800`;
|
return `${baseClasses} preset-ghost-surface-200-800`;
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
const baseClasses = 'btn flex items-center justify-start gap-2';
|
const baseClasses = 'btn flex items-center justify-start gap-2';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return `${baseClasses} preset-filled-primary-500 cursor-default`;
|
return `${baseClasses} preset-filled-primary-500 text-on-primary-500 cursor-default`;
|
||||||
}
|
}
|
||||||
return `${baseClasses} preset-ghost-surface-200-800`;
|
return `${baseClasses} preset-ghost-surface-200-800`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user