Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
{/each}
</DecorCorners>

<!-- Decore: Stripes -->
<!-- Decor: Stripes -->
<DecorStripes class="h-6" />

<!-- Bottom Row -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@
</header>
</DecorCorners>

<!-- Decore: Stripes -->
<!-- Decor: Stripes -->
<DecorStripes class="h-6" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Template data for the Plus site's templates showcase.
* Each entry includes metadata, screenshots, and per-framework download links
* for the listing and detail pages.
*/

import { getRequestEvent, query } from '$app/server';

export type TemplateCategory = 'Portfolio' | 'Blog' | 'Admin' | 'Landing';

export interface TemplateImage {
src: string;
alt: string;
}

export interface TemplateDownload {
key: string;
label: string;
href: string;
}

export interface Template {
slug: string;
name: string;
category: TemplateCategory;
tagline: string;
description: string;
isPremium: boolean;
previewUrl: string;
images: {
card: TemplateImage;
hero: TemplateImage;
screenshots: TemplateImage[];
};
stack: string[];
downloads: TemplateDownload[];
}

const placeholder = (size: string, label: string) =>
`https://placehold.co/${size}/1f1f1f/666666/webp?text=${encodeURIComponent(label)}&font=raleway`;

const placeholderDownloads: TemplateDownload[] = [
{ key: 'svelte', label: 'Svelte', href: '#' },
{ key: 'react', label: 'React', href: '#' },
{ key: 'vue', label: 'Vue', href: '#' },
{ key: 'solid', label: 'Solid.js', href: '#' },
{ key: 'astro', label: 'Astro', href: '#' },
];

const placeholderStack: string[] = ['SvelteKit', 'Svelte', 'Skeleton', 'Tailwind CSS', 'TypeScript', 'Vite'];

const templates: Template[] = [
{
slug: 'pebble',
name: 'Pebble',
category: 'Portfolio',
tagline: 'A portfolio template with case-studies, blog & contact.',
description:
'A polished portfolio template designed for designers, developers, and studios who want a focused, content-first online presence. Pebble leads with quiet typography and generous whitespace, letting your work do the talking without competing for attention. The case-study layouts are built for narrative depth — extended write-ups, inline image galleries, pull quotes, and process breakdowns are all first-class citizens, so you can walk visitors through the thinking behind a project rather than just the final pixels. A long-form blog is included for essays, notes, and changelogs, and supports tagging, reading time estimates, and a clean archive view. The contact flow is intentionally minimal: a single form, an inbox-friendly layout, and pre-wired hooks ready to drop into your transactional email provider, CRM, or service of choice. Every page is responsive down to mobile, dark-mode aware, and tuned for fast time-to-content so prospective clients land on your strongest work within a heartbeat.',
isPremium: false,
previewUrl: 'https://example.skeleton.dev',
images: {
card: { src: placeholder('1280x720', 'Pebble'), alt: 'Pebble template screenshot' },
hero: { src: placeholder('1920x960', 'Pebble — Main Hero'), alt: 'Pebble homepage hero' },
screenshots: [
{ src: placeholder('800x800', 'About'), alt: 'Pebble about page' },
{ src: placeholder('800x800', 'Case Study'), alt: 'Pebble case study page' },
{ src: placeholder('800x800', 'Blog Index'), alt: 'Pebble blog index' },
{ src: placeholder('800x800', 'Blog Post'), alt: 'Pebble blog post' },
],
},
stack: placeholderStack,
downloads: placeholderDownloads,
},
{
slug: 'pulse',
name: 'Pulse',
category: 'Blog',
tagline: 'A modern blog template with featured posts and tag pages.',
description:
'Pulse is a magazine-style blog template built for editors and small publications that ship new writing on a steady cadence. The homepage is anchored by a featured-posts hero that surfaces your latest headline alongside a curated rail of secondary stories, giving the front page the rhythm of a real publication rather than a reverse-chronological dump. Tag and author archives are first-class: every contributor gets a proper bio page with social links and a backlog of their writing, and every tag rolls up into a topical landing page you can deep-link from social or newsletters. The article layout itself is tuned for long-form reading, with comfortable measure, careful vertical rhythm, captioned figures, blockquote treatments, and inline code styling that holds up for technical posts. A built-in newsletter capture, search page, and reading-progress indicator round out the editorial toolkit, and the entire template is wired to be content-source agnostic — point it at Markdown files, a headless CMS, or your database and it will keep its shape.',
isPremium: false,
previewUrl: 'https://example.skeleton.dev',
images: {
card: { src: placeholder('1280x720', 'Pulse'), alt: 'Pulse template screenshot' },
hero: { src: placeholder('1920x960', 'Pulse — Main Hero'), alt: 'Pulse homepage hero' },
screenshots: [
{ src: placeholder('800x800', 'Article'), alt: 'Pulse article page' },
{ src: placeholder('800x800', 'Tag Archive'), alt: 'Pulse tag archive' },
{ src: placeholder('800x800', 'Author'), alt: 'Pulse author page' },
{ src: placeholder('800x800', 'Newsletter'), alt: 'Pulse newsletter signup' },
],
},
stack: placeholderStack,
downloads: placeholderDownloads,
},
{
slug: 'helix',
name: 'Helix',
category: 'Admin',
tagline: 'An admin dashboard template with analytics, billing, and settings.',
description:
'Helix is a full admin shell that gives a SaaS product everything it needs behind the login screen, without the months of internal-tools work that usually precedes a real dashboard. It opens onto an overview page with composable analytics — trend lines, sparkline tiles, segment breakdowns, and a recent-activity feed — all backed by chart primitives you can repoint at your own data. A complete billing surface covers plan selection, payment methods, invoice history, and seat-based upgrades, with empty and error states designed alongside the happy path. Team management includes invitations, role assignment, and a permissions matrix that is straightforward to extend, while the deep settings area is organized into a nested sidebar covering profile, workspace, security, integrations, notifications, and API keys. The shell uses a collapsible primary sidebar with a command palette, breadcrumbed page headers, and a slide-over panel pattern for secondary tasks, and every screen is responsive down to mobile so on-call work from a phone is genuinely usable rather than a fallback.',
isPremium: true,
previewUrl: 'https://example.skeleton.dev',
images: {
card: { src: placeholder('1280x720', 'Helix'), alt: 'Helix template screenshot' },
hero: { src: placeholder('1920x960', 'Helix — Main Hero'), alt: 'Helix dashboard hero' },
screenshots: [
{ src: placeholder('800x800', 'Dashboard'), alt: 'Helix dashboard' },
{ src: placeholder('800x800', 'Analytics'), alt: 'Helix analytics' },
{ src: placeholder('800x800', 'Billing'), alt: 'Helix billing' },
{ src: placeholder('800x800', 'Team'), alt: 'Helix team management' },
],
},
stack: placeholderStack,
downloads: placeholderDownloads,
},
{
slug: 'quantum',
name: 'Quantum',
category: 'Landing',
tagline: 'A high-conversion landing page template for product launches.',
description:
'Quantum is a marketing-first landing page template engineered around the rhythm of a high-conversion launch sequence, where every section earns its place by moving the reader one step closer to a decision. The hero pairs a confident headline slot with a supporting subhead, primary and secondary calls to action, and an integrated product visual that holds its own on widescreen and on mobile. Beneath it sits a social-proof band — logos, ratings, and a press strip — followed by a feature bento that mixes large narrative tiles with smaller spec callouts, so you can sell both the big idea and the concrete details on the same scroll. A pricing section ships with monthly/annual toggles, a recommended-plan accent, and tiered feature comparison; a testimonial block, FAQ accordion, and final CTA round out the page with the kind of objection-handling that actually moves a launch from interest to signup. Designed to drop in cleanly, restyle to your brand in an afternoon, and convert visitors on first read.',
isPremium: true,
previewUrl: 'https://example.skeleton.dev',
images: {
card: { src: placeholder('1280x720', 'Quantum'), alt: 'Quantum template screenshot' },
hero: { src: placeholder('1920x960', 'Quantum — Main Hero'), alt: 'Quantum landing hero' },
screenshots: [
{ src: placeholder('800x800', 'Features'), alt: 'Quantum features section' },
{ src: placeholder('800x800', 'Pricing'), alt: 'Quantum pricing section' },
{ src: placeholder('800x800', 'Testimonials'), alt: 'Quantum testimonials' },
{ src: placeholder('800x800', 'FAQ'), alt: 'Quantum FAQ section' },
],
},
stack: placeholderStack,
downloads: placeholderDownloads,
},
];

/** Get all templates */
export const getTemplates = query(async (): Promise<Template[]> => {
return templates;
});

/** Get a single template by slug (matches route param `slug`) */
export const getTemplate = query(async (): Promise<Template | undefined> => {
const { params } = getRequestEvent();
return templates.find((t) => t.slug === (params as Record<string, string>).slug);
});
13 changes: 13 additions & 0 deletions sites/plus.skeleton.dev/src/lib/state/plus.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Simulated Plus subscription state.
*
* Will be replaced by a real account-derived value once Plus subscriptions
* are wired up. Until then, components read `plusState.unlocked` to decide
* between "Download" and "Unlock with Plus" affordances, and dev/tooling
* can flip the value via `plusState.unlocked = true`.
*/
class PlusState {
unlocked = $state(false);
}

export const plusState = new PlusState();
4 changes: 2 additions & 2 deletions sites/plus.skeleton.dev/src/routes/(app)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</div>
</section>

<!-- Decore: Stripes -->
<!-- Decor: Stripes -->
<DecorStripes class="h-6" />

<!-- Features -->
Expand Down Expand Up @@ -72,7 +72,7 @@
</div>
</DecorCorners>

<!-- Decore: Stripes -->
<!-- Decor: Stripes -->
<DecorStripes class="h-6" />

<!-- Themes Repository -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
<script lang="ts">
import PageHeader from '$lib/components/layout/page-header.svelte';
import { getTemplates } from '$lib/remote/templates/get-templates.remote';
import { plusState } from '$lib/state/plus.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
import LockIcon from '@lucide/svelte/icons/lock';

const templates = $derived(await getTemplates());
</script>

<PageHeader title="Templates">
{#snippet description()}
<p class="opacity-60">Ready-to-use website templates built with Skeleton.</p>
{/snippet}
{#snippet trail()}
<a href="/overview/pricing" class="btn preset-filled">
<LockIcon />
<span>Unlock All Templates</span>
</a>
{/snippet}
</PageHeader>

<div class="container-page">
<p>Now viewing <code class="code">templates</code></p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 lg:gap-8">
{#each templates as template (template.slug)}
{@const locked = template.isPremium && !plusState.unlocked}
<article class="card bg-surface-50-950 border border-surface-200-800 overflow-hidden">
<!-- Image -->
<a href="/content/templates/{template.slug}" class="block p-4 pb-0">
<img
src={template.images.card.src}
alt={template.images.card.alt}
class="w-full aspect-video rounded-container border border-surface-200-800"
/>
</a>
<!-- Footer -->
<footer class="p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<!-- Title + Category -->
<a href="/content/templates/{template.slug}" class="space-y-0.5">
<div class="flex items-center gap-2">
<h2 class="h4">{template.name}</h2>
{#if template.isPremium}
<span class="badge preset-tonal">PLUS</span>
{/if}
</div>
<p class="text-sm opacity-60">{template.category}</p>
</a>
<!-- Actions -->
<div class="flex items-center gap-2">
<a
href={template.previewUrl}
target="_blank"
rel="noopener noreferrer"
class="btn preset-tonal"
aria-label="Preview {template.name}"
>
<span>Preview</span>
<ArrowUpRightIcon />
</a>
{#if locked}
<a href="/overview/pricing" class="btn preset-filled" aria-label="Unlock {template.name} with Plus">
<LockIcon />
<span>Unlock with Plus</span>
</a>
{:else}
<a href="/content/templates/{template.slug}" class="btn preset-filled" aria-label="Download {template.name}">
<span>View</span>
<ArrowRight />
</a>
{/if}
</div>
</footer>
</article>
{/each}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script lang="ts">
import DecorStripes from '$lib/components/layout/decor-stripes.svelte';
import PageHeader from '$lib/components/layout/page-header.svelte';
import { getTemplate } from '$lib/remote/templates/get-templates.remote';
import { plusState } from '$lib/state/plus.svelte';
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import DownloadIcon from '@lucide/svelte/icons/download';
import LockIcon from '@lucide/svelte/icons/lock';
import { Menu, Portal } from '@skeletonlabs/skeleton-svelte';

const template = $derived(await getTemplate());
const locked = $derived(!!template?.isPremium && !plusState.unlocked);
</script>

{#if template}
<PageHeader title={template.name}>
{#snippet description()}
<p class="opacity-60">{template.tagline}</p>
{/snippet}
{#snippet trail()}
<a href={template.previewUrl} target="_blank" rel="noopener noreferrer" class="btn preset-outlined">
<ArrowUpRightIcon />
<span>Preview</span>
</a>
{#if locked}
<a href="/overview/pricing" class="btn preset-filled">
<LockIcon />
<span>Unlock with Plus</span>
</a>
{:else}
<Menu positioning={{ placement: 'bottom-end' }}>
<Menu.Trigger class="btn preset-filled">
<DownloadIcon />
<span>Download</span>
<ChevronDownIcon class="size-elem-sm opacity-60" />
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content class="z-50">
<Menu.ItemGroup>
<Menu.ItemGroupLabel>Choose your framework</Menu.ItemGroupLabel>
{#each template.downloads as fw (fw.key)}
<Menu.Item value={fw.key}>
{#snippet element(attributes: Record<string, unknown>)}
<a {...attributes} href={fw.href} download>
<Menu.ItemText>{fw.label}</Menu.ItemText>
</a>
{/snippet}
</Menu.Item>
{/each}
</Menu.ItemGroup>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu>
{/if}
{/snippet}
</PageHeader>

<!-- Hero Screenshot -->
<img src={template.images.hero.src} alt={template.images.hero.alt} class="w-full aspect-video border-b border-surface-200-800" />

<!-- Decor: Stripes -->
<DecorStripes class="h-6" />

<!-- Screenshot Grid -->
{#if template.images.screenshots.length > 0}
<div
class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 border-t border-b border-surface-200-800 divide-x divide-y lg:divide-y-0 divide-surface-200-800"
>
{#each template.images.screenshots as screenshot (screenshot.src)}
<img src={screenshot.src} alt={screenshot.alt} class="w-full aspect-square" />
{/each}
</div>
{/if}

<div class="grid grid-cols-1 lg:grid-cols-[75%_25%] divide-y lg:divide-y-0 lg:divide-x divide-surface-200-800">
<!-- About -->
<section class="container-cell space-y-2">
<h2 class="h2">About</h2>
<p class="opacity-60">{template.description}</p>
</section>

<!-- Stack -->
<section class="container-cell space-y-4" aria-label="Stack">
{#each template.stack as name (name)}
<p class="font-medium">{name}</p>
{/each}
</section>
</div>
{/if}