Files
SnapOtter/apps/docs/.vitepress/theme/DocsHome.vue
T
SnapOtterandGitHub a3301e0a3a docs: keep hero install command on one line and make the whole box copyable (#330)
The docs.snapotter.com hero command wrapped onto two lines. Switch the
command from pre-wrap to nowrap with internal horizontal scroll, widen the
box, and nudge the font down so it sits on one line on desktop and scrolls
inside its box on mobile without the page overflowing.

Move the copy action onto the whole command box (pointer cursor + hover
affordance) so clicking anywhere copies, while keeping the real Copy button
(now click.stop so it does not double-fire, with a min-width to avoid layout
shift between the Copy and Copied states).
2026-06-22 22:48:40 +08:00

177 lines
10 KiB
Vue

<script setup lang="ts">
const command = "docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest";
const selfLinks = [
{ label: "Quick start", href: "/guide/getting-started#quick-start" },
{ label: "Configuration", href: "/guide/configuration" },
{ label: "Hardware & sizing", href: "/guide/deployment#hardware-requirements" },
{ label: "Database & backups", href: "/guide/database" },
{ label: "Docker tags & GPU", href: "/guide/docker-tags" },
{ label: "Supported formats", href: "/guide/supported-formats" },
];
const entLinks = [
{ label: "Architecture", href: "/guide/architecture" },
{ label: "Security & hardening", href: "/guide/security" },
{ label: "SSO · SAML · OIDC", href: "/guide/oidc" },
{ label: "SCIM provisioning", href: "/guide/scim" },
{ label: "Users, roles & audit", href: "/guide/users-roles" },
{ label: "Compliance & SBOM", href: "/guide/security#compliance-artifacts" },
];
const modalities = [
{ label: "Image", count: 64, href: "/tools/resize" },
{ label: "Video", count: 29, href: "/tools/convert-video" },
{ label: "Audio", count: 17, href: "/tools/convert-audio" },
{ label: "PDF", count: 37, href: "/tools/merge-pdf" },
{ label: "Data", count: 10, href: "/tools/chart-maker" },
];
const shared = [
{ label: "REST API", sub: "Keys, endpoints & OpenAPI", href: "/api/rest" },
{ label: "Changelog", sub: "What's new in 2.0", href: "/changelog" },
{ label: "llms.txt", sub: "AI-friendly docs", href: "/llms.txt" },
];
import { ref } from "vue";
const copyLabel = ref("Copy");
function copyCommand() {
navigator.clipboard?.writeText(command);
copyLabel.value = "Copied!";
setTimeout(() => { copyLabel.value = "Copy"; }, 1500);
}
</script>
<template>
<div class="so-home">
<section class="hero">
<p class="eyebrow">Self-hosted · Open source · AGPLv3</p>
<h1 class="hero-title">SnapOtter Documentation</h1>
<p class="hero-sub">
<strong>157 tools</strong> for image, video, audio, PDF &amp; data, running entirely on your
hardware. Choose your path below, or get running in one command:
</p>
<div class="cmd" title="Click to copy" @click="copyCommand">
<code>$ {{ command }}</code>
<button class="copy" type="button" aria-label="Copy command" @click.stop="copyCommand">{{ copyLabel }}</button>
</div>
<p class="hero-meta">
<a href="/guide/getting-started">Full install guide</a> ·
<a href="/guide/getting-started#docker-compose">GPU &amp; Compose setup</a> ·
<a href="https://demo.snapotter.com">Try the live demo</a>
</p>
</section>
<section class="doors">
<div class="door self">
<div class="door-head">
<span class="door-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/></svg>
</span>
<h2>Self-hosting</h2>
</div>
<p class="door-sub">Get SnapOtter running and keep it healthy.</p>
<ul class="door-links">
<li v-for="l in selfLinks" :key="l.href"><a :href="l.href">{{ l.label }}</a></li>
</ul>
<a class="door-cta" href="/guide/getting-started">Start self-hosting </a>
</div>
<div class="door ent">
<div class="door-head">
<span class="door-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M5 21V7l8-4v18M19 21V11l-6-4"/></svg>
</span>
<h2>Enterprise</h2>
</div>
<p class="door-sub">Evaluate, secure &amp; govern your deployment.</p>
<ul class="door-links">
<li v-for="l in entLinks" :key="l.href"><a :href="l.href">{{ l.label }}</a></li>
</ul>
<a class="door-cta" href="/guide/architecture">Evaluate for your org </a>
</div>
</section>
<section class="mod">
<p class="mod-head"><strong>157 tools across 5 modalities</strong> <span>browse the full reference by type</span></p>
<div class="chips">
<a v-for="m in modalities" :key="m.href" class="chip" :href="m.href">
<span class="chip-label">{{ m.label }}</span>
<span class="chip-count">{{ m.count }} tools</span>
</a>
</div>
</section>
<section class="shared">
<a v-for="s in shared" :key="s.href" class="scard" :href="s.href">
<strong>{{ s.label }}</strong>
<span>{{ s.sub }}</span>
</a>
</section>
</div>
</template>
<style scoped>
.so-home { max-width: 1080px; margin: 0 auto; padding: 16px 24px 64px; }
.hero { text-align: center; padding: 48px 16px 28px; }
.eyebrow { font: 600 11px/1 var(--vp-font-family-mono); letter-spacing: .16em; text-transform: uppercase; color: #A85518; margin-bottom: 14px; }
.hero-title { font-family: var(--so-font-heading); font-size: 42px; font-weight: 800; letter-spacing: -.03em; margin-bottom: 12px; }
.hero-sub { color: var(--vp-c-text-2); font-size: 17px; max-width: 60ch; margin: 0 auto 20px; }
.cmd { display: flex; gap: 14px; align-items: center; max-width: 860px; margin: 0 auto; background: #15100B; border: 1px solid #3A2A1E; border-radius: 10px; padding: 13px 16px; text-align: left; cursor: pointer; transition: border-color .15s, background-color .15s; }
.cmd:hover { border-color: #5A4636; background: #1B140D; }
.cmd code { flex: 1; min-width: 0; font: 600 13px/1.5 var(--vp-font-family-mono); color: #FFD9B0; white-space: nowrap; overflow-x: auto; scrollbar-width: none; }
.cmd code::-webkit-scrollbar { display: none; }
.copy { flex: none; min-width: 62px; text-align: center; font: 600 10.5px/1 var(--vp-font-family-mono); color: #cbb9a6; border: 1px solid #4a3a2c; border-radius: 6px; padding: 8px 11px; background: transparent; cursor: pointer; transition: border-color .15s, color .15s; }
.copy:hover { border-color: #6a5340; color: #FFD9B0; }
.hero-meta { margin-top: 12px; font-size: 13px; color: var(--vp-c-text-2); }
.hero-meta a { color: #A85518; font-weight: 600; }
.doors { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.door { border: 1px solid var(--vp-c-border); border-radius: 16px; padding: 24px; transition: transform .15s, box-shadow .15s; }
.door:hover { transform: translateY(-3px); box-shadow: 0 22px 44px -26px rgba(20,12,4,.5); }
.door.self { background: linear-gradient(180deg, #FFF5ED, #fff); }
.door.ent { background: linear-gradient(165deg, #241A13, #15100B); color: #F0EBE4; border-color: #3A2A1E; }
.door-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.door-icon { width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center; flex: none; }
.door.self .door-icon { background: #E07832; color: #fff; }
.door.ent .door-icon { background: #3A2A1E; color: #F09550; }
.door-head h2 { font-family: var(--so-font-heading); font-size: 22px; font-weight: 700; border: 0; padding: 0; margin: 0; }
.door-sub { font-size: 13.5px; margin-bottom: 18px; }
.door.self .door-sub { color: var(--vp-c-text-2); }
.door.ent .door-sub { color: #C9BCAE; }
.door-links { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; list-style: none; margin: 0 0 20px; padding: 0; }
.door-links li { border-bottom: 1px solid; padding: 0; }
.door.self .door-links li { border-color: #EBDDCC; }
.door.ent .door-links li { border-color: #3A2C20; }
.door-links a { display: block; padding: 10px 2px; font-size: 13.5px; font-weight: 500; text-decoration: none; color: inherit; }
.door.self .door-links a:hover { color: #A85518; }
.door.ent .door-links a:hover { color: #F09550; }
.door-cta { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: 13.5px; padding: 11px 18px; border-radius: 10px; text-decoration: none; }
.door.self .door-cta { background: #A85518; color: #fff; }
.door.ent .door-cta { background: #F09550; color: #231A12; }
.mod { padding: 28px 0 6px; }
.mod-head { margin-bottom: 13px; }
.mod-head strong { font-family: var(--so-font-heading); font-size: 15px; }
.mod-head span { font-size: 12.5px; color: var(--vp-c-text-2); margin-left: 8px; }
.chips { display: grid; grid-template-columns: repeat(5, 1fr); gap: 11px; }
.chip { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--vp-c-border); border-radius: 11px; padding: 12px 13px; text-decoration: none; transition: border-color .15s, transform .15s; }
.chip:hover { border-color: #E07832; transform: translateY(-2px); }
.chip-label { font-size: 12.5px; font-weight: 600; color: var(--vp-c-text-1); }
.chip-count { font: 700 12px/1 var(--vp-font-family-mono); color: #A85518; }
.shared { display: grid; grid-template-columns: repeat(3, 1fr); gap: 13px; padding: 18px 0 0; }
.scard { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--vp-c-border); border-radius: 12px; padding: 14px 15px; text-decoration: none; transition: border-color .15s, transform .15s; }
.scard:hover { border-color: #E07832; transform: translateY(-2px); }
.scard strong { font-family: var(--so-font-heading); font-size: 13.5px; color: var(--vp-c-text-1); }
.scard span { font-size: 11.5px; color: var(--vp-c-text-2); }
:root.dark .chip-label, :root.dark .scard strong { color: var(--vp-c-text-1); }
:root.dark .door.self { background: linear-gradient(180deg, #2A1F16, #221A13); border-color: #3A2C20; color: #F0EBE4; }
:root.dark .door.self .door-links li { border-color: #3A2C20; }
:root.dark .chip-count { color: #F0A766; }
:root.dark .hero-meta a, :root.dark .eyebrow { color: #F0A766; }
:root.dark .door.self .door-sub { color: #C9BCAE; }
:root.dark .door.self .door-links a:hover { color: #F09550; }
:root.dark .door.self .door-cta { background: #A85518; }
@media (max-width: 840px) {
.doors, .shared { grid-template-columns: 1fr; }
.chips { grid-template-columns: repeat(2, 1fr); }
}
</style>