ci(site): add format/lint/test/build workflow for astro site

Add Site GitHub Actions workflow running prettier format check, astro
check, tests, and build on changes under site/. Wire prettier into the
site package with config, ignore, and scripts, and format existing
sources.
This commit is contained in:
Christian Visintin
2026-06-07 23:10:24 +02:00
parent f92cb93755
commit a2d766d688
19 changed files with 445 additions and 107 deletions

6
site/.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
dist
node_modules
.astro
package-lock.json
public/install.sh
public/install.ps1

11
site/.prettierrc.json Normal file
View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

44
site/package-lock.json generated
View File

@@ -14,6 +14,8 @@
"devDependencies": {
"@astrojs/check": "^0.9.9",
"@tailwindcss/vite": "^4.0.0",
"prettier": "3.8.3",
"prettier-plugin-astro": "0.14.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.0"
}
@@ -5717,6 +5719,21 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-astro": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz",
"integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.9.1",
"prettier": "^3.0.0",
"sass-formatter": "^0.7.6"
},
"engines": {
"node": "^14.15.0 || >=16.0.0"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -6066,6 +6083,23 @@
"fsevents": "~2.3.2"
}
},
"node_modules/s.color": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz",
"integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==",
"dev": true,
"license": "MIT"
},
"node_modules/sass-formatter": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz",
"integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"suf-log": "^2.5.3"
}
},
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
@@ -6256,6 +6290,16 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/suf-log": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
"integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==",
"dev": true,
"license": "MIT",
"dependencies": {
"s.color": "0.0.15"
}
},
"node_modules/svgo": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",

View File

@@ -9,7 +9,9 @@
"prebuild": "node scripts/copy-install.mjs",
"build": "astro build",
"preview": "astro preview",
"check": "astro check"
"check": "astro check",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@astrojs/sitemap": "^3.2.0",
@@ -18,6 +20,8 @@
"devDependencies": {
"@astrojs/check": "^0.9.9",
"@tailwindcss/vite": "^4.0.0",
"prettier": "3.8.3",
"prettier-plugin-astro": "0.14.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.0"
}

View File

@@ -1,22 +1,24 @@
import { copyFile, access } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { copyFile, access } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
// repo-root install scripts (single source of truth)
const scripts = ['install.sh', 'install.ps1'];
const scripts = ["install.sh", "install.ps1"];
await Promise.all(
scripts.map(async (name) => {
const src = join(here, '..', '..', name);
const dest = join(here, '..', 'public', name);
const src = join(here, "..", "..", name);
const dest = join(here, "..", "public", name);
try {
await access(src);
} catch {
console.error(`[copy-install] source not found: ${src}`);
console.error(`[copy-install] the repo-root ${name} must be available at build time.`);
console.error(
`[copy-install] the repo-root ${name} must be available at build time.`,
);
process.exit(1);
}

View File

@@ -5,6 +5,7 @@ interface Props {
}
const { text, class: className } = Astro.props;
---
<button
type="button"
class:list={[
@@ -15,11 +16,29 @@ const { text, class: className } = Astro.props;
aria-label="Copy command to clipboard"
title="Copy"
>
<svg class="copy-icon h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<svg
class="copy-icon h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon hidden h-4 w-4 text-green" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<svg
class="check-icon hidden h-4 w-4 text-green"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5"></path>
</svg>
</button>
@@ -46,7 +65,9 @@ const { text, class: className } = Astro.props;
};
document.addEventListener("click", (event) => {
const button = (event.target as HTMLElement)?.closest<HTMLElement>(".copy-btn");
const button = (event.target as HTMLElement)?.closest<HTMLElement>(
".copy-btn",
);
if (button) {
handleCopy(button);
}

View File

@@ -17,44 +17,98 @@ const remote = [
];
const status = "↹ switch · ↑↓ move · ↵ open · SPACE transfer";
---
<section class="mx-auto max-w-5xl px-4 pt-12">
<p class="text-green text-sm tracking-widest uppercase">// terminal file transfer</p>
<div class="mt-4 overflow-hidden rounded-lg border border-line bg-mantle shadow-2xl">
<p class="text-green text-sm tracking-widest uppercase">
// terminal file transfer
</p>
<div
class="mt-4 overflow-hidden rounded-lg border border-line bg-mantle shadow-2xl"
>
<div class="border-b border-line bg-crust px-3 py-2 text-xs text-overlay">
termscp — 192.168.1.10 (sftp) connected<span class="blink ml-1"></span>
</div>
<div class="grid grid-cols-2 bg-base">
<div>
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-teal">/home/veeso (local)</div>
{local.map((r) => (
<div class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${r.kind === "dir" ? "text-blue" : "text-text"}`}>
<span class="truncate min-w-0">{r.name}</span><span class="text-overlay shrink-0">{r.size}</span>
</div>
))}
<div
class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-teal"
>
/home/veeso (local)
</div>
{
local.map((r) => (
<div
class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${r.kind === "dir" ? "text-blue" : "text-text"}`}
>
<>
<span class="truncate min-w-0">{r.name}</span>
<span class="text-overlay shrink-0">{r.size}</span>
</>
</div>
))
}
</div>
<div class="border-l border-line">
<div class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-mauve">/var/www (remote)</div>
{remote.map((r) => (
<div class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${
r.kind === "sel" ? "bg-blue text-base font-bold" : r.kind === "dir" ? "text-blue" : "text-text"
}`}>
<span class="truncate min-w-0">{r.name}</span>
<span class={`shrink-0 ${r.kind === "sel" ? "text-base" : "text-overlay"}`}>{r.size}</span>
</div>
))}
<div
class="border-b border-line bg-mantle px-3 py-1.5 text-xs text-mauve"
>
/var/www (remote)
</div>
{
remote.map((r) => (
<div
class={`flex justify-between gap-2 px-3 py-0.5 text-sm ${
r.kind === "sel"
? "bg-blue text-base font-bold"
: r.kind === "dir"
? "text-blue"
: "text-text"
}`}
>
<span class="truncate min-w-0">{r.name}</span>
<span
class={`shrink-0 ${r.kind === "sel" ? "text-base" : "text-overlay"}`}
>
{r.size}
</span>
</div>
))
}
</div>
</div>
{/* set:html is safe: status is a static author-controlled string, never user input. */}
<div class="flex justify-between border-t border-line bg-crust px-3 py-1.5 text-xs text-overlay">
<span set:html={status.replace(/(↹|↑↓|↵|SPACE)/g, '<span class="text-yellow">$1</span>')}></span>
{
/* set:html is safe: status is a static author-controlled string, never user input. */
}
<div
class="flex justify-between border-t border-line bg-crust px-3 py-1.5 text-xs text-overlay"
>
<span
set:html={status.replace(
/(↹|↑↓|↵|SPACE)/g,
'<span class="text-yellow">$1</span>',
)}
/>
<span>4 files · 1.2 GB</span>
</div>
<div class="bg-base px-6 py-6 text-center">
<h1 class="text-2xl font-bold text-text">The dual-pane explorer for your terminal</h1>
<p class="mx-auto mt-2 max-w-xl text-subtext">Local left, remote right. Transfer, sync, edit — never leave the keyboard.</p>
<h1 class="text-2xl font-bold text-text">
The dual-pane explorer for your terminal
</h1>
<p class="mx-auto mt-2 max-w-xl text-subtext">
Local left, remote right. Transfer, sync, edit — never leave the
keyboard.
</p>
<div class="mt-5 flex flex-wrap justify-center gap-3">
<a href="/install" class="rounded-lg bg-green px-5 py-2.5 font-bold text-crust">Install termscp →</a>
<a href={DOCS_URL} class="rounded-lg border border-line px-5 py-2.5 text-text">User manual</a>
<a
href="/install"
class="rounded-lg bg-green px-5 py-2.5 font-bold text-crust"
>Install termscp →</a
>
<a
href={DOCS_URL}
class="rounded-lg border border-line px-5 py-2.5 text-text"
>User manual</a
>
</div>
</div>
</div>

View File

@@ -1,7 +1,11 @@
---
interface Props { title: string; body: string; }
interface Props {
title: string;
body: string;
}
const { title, body } = Astro.props;
---
<div class="rounded-lg border border-line bg-mantle p-5">
<h3 class="text-green">{title}</h3>
<p class="mt-2 text-sm text-subtext">{body}</p>

View File

@@ -2,13 +2,20 @@
import { DOCS_URL, GITHUB_URL, VERSION } from "../consts";
const year = new Date().getFullYear();
---
<footer class="mt-auto border-t border-line bg-mantle">
<div class="mx-auto flex max-w-5xl flex-col items-center gap-2 px-4 py-8 text-sm text-overlay">
<div
class="mx-auto flex max-w-5xl flex-col items-center gap-2 px-4 py-8 text-sm text-overlay"
>
<div class="flex gap-5">
<a href={GITHUB_URL} class="hover:text-text">GitHub</a>
<a href="https://crates.io/crates/termscp" class="hover:text-text">crates.io</a>
<a href="https://crates.io/crates/termscp" class="hover:text-text"
>crates.io</a
>
<a href={DOCS_URL} class="hover:text-text">User manual</a>
</div>
<p>termscp v{VERSION} · © {year} Christian Visintin · Released under the MIT license.</p>
<p>
termscp v{VERSION} · © {year} Christian Visintin · Released under the MIT license.
</p>
</div>
</footer>

View File

@@ -1,35 +1,52 @@
---
import CopyButton from "./CopyButton.astro";
interface Method { id: string; label: string; cmd: string; }
interface Props { methods: Method[]; }
interface Method {
id: string;
label: string;
cmd: string;
}
interface Props {
methods: Method[];
}
const { methods } = Astro.props;
---
<div class="rounded-lg border border-line bg-mantle">
<div class="flex flex-wrap gap-1 border-b border-line p-2" role="tablist">
{methods.map((m, i) => (
<button
class="install-tab rounded px-3 py-1 text-sm text-overlay data-[active]:bg-base data-[active]:text-green"
data-target={m.id}
data-active={i === 0 ? "" : undefined}
>{m.label}</button>
))}
{
methods.map((m, i) => (
<button
class="install-tab rounded px-3 py-1 text-sm text-overlay data-[active]:bg-base data-[active]:text-green"
data-target={m.id}
data-active={i === 0 ? "" : undefined}
>
{m.label}
</button>
))
}
</div>
{methods.map((m, i) => (
<div class="install-panel relative" data-id={m.id} hidden={i !== 0}>
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{m.cmd}</code></pre>
<CopyButton text={m.cmd} class="absolute right-2 top-2" />
</div>
))}
{
methods.map((m, i) => (
<div class="install-panel relative" data-id={m.id} hidden={i !== 0}>
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text">
<code>{m.cmd}</code>
</pre>
<CopyButton text={m.cmd} class="absolute right-2 top-2" />
</div>
))
}
</div>
<script is:inline>
document.querySelectorAll(".install-tab").forEach((tab) => {
tab.addEventListener("click", () => {
const id = tab.getAttribute("data-target");
document.querySelectorAll(".install-tab").forEach((t) =>
t.toggleAttribute("data-active", t === tab));
document.querySelectorAll(".install-panel").forEach((p) =>
(p.hidden = p.getAttribute("data-id") !== id));
document
.querySelectorAll(".install-tab")
.forEach((t) => t.toggleAttribute("data-active", t === tab));
document
.querySelectorAll(".install-panel")
.forEach((p) => (p.hidden = p.getAttribute("data-id") !== id));
});
});
</script>

View File

@@ -2,7 +2,10 @@
import ThemeToggle from "./ThemeToggle.astro";
import { DOCS_URL, GITHUB_URL } from "../consts";
---
<header class="sticky top-0 z-50 border-b border-line bg-mantle/90 backdrop-blur">
<header
class="sticky top-0 z-50 border-b border-line bg-mantle/90 backdrop-blur"
>
<nav class="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
<a href="/" class="flex items-center gap-2 text-text">
<img src="/assets/images/termscp.webp" alt="termscp" class="h-7 w-7" />
@@ -19,8 +22,13 @@ import { DOCS_URL, GITHUB_URL } from "../consts";
<!-- Mobile -->
<details class="relative sm:hidden">
<summary class="cursor-pointer list-none rounded p-2 text-text hover:bg-mantle" aria-label="Menu">☰</summary>
<div class="absolute right-0 mt-2 flex w-48 flex-col gap-3 rounded-lg border border-line bg-mantle p-4 text-sm shadow-xl">
<summary
class="cursor-pointer list-none rounded p-2 text-text hover:bg-mantle"
aria-label="Menu">☰</summary
>
<div
class="absolute right-0 mt-2 flex w-48 flex-col gap-3 rounded-lg border border-line bg-mantle p-4 text-sm shadow-xl"
>
<a href="/install" class="text-overlay hover:text-text">Install</a>
<a href={DOCS_URL} class="text-overlay hover:text-text">User manual</a>
<a href={GITHUB_URL} class="text-overlay hover:text-text">GitHub</a>

View File

@@ -1,11 +1,18 @@
---
const protocols = ["SCP", "SFTP", "FTP/S", "S3", "Kube", "SMB", "WebDAV"];
---
<section class="mx-auto max-w-5xl px-4 py-12 text-center">
<h2 class="text-overlay text-sm uppercase tracking-widest">Speaks every protocol</h2>
<h2 class="text-overlay text-sm uppercase tracking-widest">
Speaks every protocol
</h2>
<div class="mt-4 flex flex-wrap justify-center gap-2">
{protocols.map((proto) => (
<span class="rounded-full border border-line px-4 py-1 text-sm text-text">{proto}</span>
))}
{
protocols.map((proto) => (
<span class="rounded-full border border-line px-4 py-1 text-sm text-text">
{proto}
</span>
))
}
</div>
</section>

View File

@@ -1,6 +1,7 @@
---
// Theme toggle. Contract: localStorage 'theme' = "frappe" | "latte". Safe to render multiple times.
---
<button
type="button"
data-theme-toggle
@@ -17,15 +18,22 @@
window.__themeToggleInit = true;
const root = document.documentElement;
const btns = document.querySelectorAll("[data-theme-toggle]");
const setPressed = () => btns.forEach((b) =>
b.setAttribute("aria-pressed", String(root.getAttribute("data-theme") === "latte")));
const setPressed = () =>
btns.forEach((b) =>
b.setAttribute(
"aria-pressed",
String(root.getAttribute("data-theme") === "latte"),
),
);
btns.forEach((b) =>
b.addEventListener("click", () => {
const next = root.getAttribute("data-theme") === "frappe" ? "latte" : "frappe";
const next =
root.getAttribute("data-theme") === "frappe" ? "latte" : "frappe";
root.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
setPressed();
}));
}),
);
setPressed();
})();
</script>

View File

@@ -30,7 +30,10 @@ const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:alt" content="termscp — terminal file transfer and explorer" />
<meta
property="og:image:alt"
content="termscp — terminal file transfer and explorer"
/>
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="640" />
@@ -41,24 +44,51 @@ const ogImage = new URL("/assets/images/og_preview.jpg", site).href;
<meta name="twitter:image" content={ogImage} />
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="96x96" href="/assets/images/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/favicon-16x16.png" />
<link
rel="icon"
type="image/png"
sizes="96x96"
href="/assets/images/favicon-96x96.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/assets/images/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/images/favicon-16x16.png"
/>
<!-- Font preload -->
<link rel="preload" href="/fonts/jetbrains-mono-400.woff2" as="font" type="font/woff2" crossorigin />
<link
rel="preload"
href="/fonts/jetbrains-mono-400.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<!-- No-flash theme bootstrap. Contract: localStorage 'theme' is "frappe" | "latte"; ThemeToggle writes the same. -->
<script is:inline>
(() => {
const t = localStorage.getItem("theme") ||
(matchMedia("(prefers-color-scheme: light)").matches ? "latte" : "frappe");
const t =
localStorage.getItem("theme") ||
(matchMedia("(prefers-color-scheme: light)").matches
? "latte"
: "frappe");
document.documentElement.setAttribute("data-theme", t);
})();
</script>
<!-- Umami analytics -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="ad2d5621-a893-488c-baa6-d39c306374cf"></script>
<script
defer
src="https://cloud.umami.is/script.js"
data-website-id="ad2d5621-a893-488c-baa6-d39c306374cf"></script>
</head>
<body class="bg-base text-text font-mono min-h-screen flex flex-col">
<slot />

View File

@@ -7,14 +7,30 @@ import ProtocolStrip from "../components/ProtocolStrip.astro";
import FeatureCard from "../components/FeatureCard.astro";
const features = [
{ title: "Handy UI", body: "Explore and operate on the remote and local file system with a handy UI." },
{
title: "Handy UI",
body: "Explore and operate on the remote and local file system with a handy UI.",
},
{ title: "Cross platform", body: "Runs on Windows, macOS, Linux and BSD." },
{ title: "Customizable", body: "Customize the explorer, the text editor and default options." },
{ title: "Bookmarks", body: "Connect to favourite hosts via bookmarks and recent connections." },
{ title: "Security first", body: "Store passwords in your operating system key vault." },
{ title: "Eye on performance", body: "Built with an eye on performance to keep CPU usage low." },
{
title: "Customizable",
body: "Customize the explorer, the text editor and default options.",
},
{
title: "Bookmarks",
body: "Connect to favourite hosts via bookmarks and recent connections.",
},
{
title: "Security first",
body: "Store passwords in your operating system key vault.",
},
{
title: "Eye on performance",
body: "Built with an eye on performance to keep CPU usage low.",
},
];
---
<Base
title="termscp — terminal file transfer & explorer for SCP/SFTP/FTP/S3/Kube/SMB/WebDAV"
description="A feature-rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB."
@@ -26,9 +42,7 @@ const features = [
<section class="mx-auto max-w-5xl px-4 py-12">
<h2 class="mb-6 text-center text-xl text-text">Why termscp</h2>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{features.map((f) => (
<FeatureCard title={f.title} body={f.body} />
))}
{features.map((f) => <FeatureCard title={f.title} body={f.body} />)}
</div>
</section>
<ProtocolStrip />

View File

@@ -6,7 +6,8 @@ import InstallTabs from "../components/InstallTabs.astro";
import CopyButton from "../components/CopyButton.astro";
import { DOCS_URL } from "../consts";
const script = "curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh";
const script =
"curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh";
const psScript = "irm https://termscp.rs/install.ps1 | iex";
const methods = [
@@ -16,6 +17,7 @@ const methods = [
{ id: "choco", label: "Windows (Chocolatey)", cmd: "choco install termscp" },
];
---
<Base
title="Install termscp"
description="Install termscp on Linux, macOS, BSD and Windows — one shell command, or via Cargo, pacman, pkgin and Chocolatey."
@@ -24,37 +26,58 @@ const methods = [
<Nav />
<main class="mx-auto w-full max-w-3xl flex-1 px-4 py-12">
<h1 class="text-2xl font-bold text-text">Install termscp</h1>
<p class="mt-2 text-subtext">Pick your platform. One command and you're in.</p>
<p class="mt-2 text-subtext">
Pick your platform. One command and you're in.
</p>
<div class="mt-6 rounded-lg border border-line bg-mantle p-1">
<div class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay">
<div
class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay"
>
<span class="text-green">$</span> Linux · macOS · BSD
</div>
<div class="relative">
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{script}</code></pre>
<pre
class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{script}</code></pre>
<CopyButton text={script} class="absolute right-2 top-2" />
</div>
</div>
<p class="mt-2 text-xs text-overlay">Requires <span class="text-text">curl</span>. macOS uses Homebrew if available, otherwise builds from source.</p>
<p class="mt-2 text-xs text-overlay">
Requires <span class="text-text">curl</span>. macOS uses Homebrew if
available, otherwise builds from source.
</p>
<div class="mt-6 rounded-lg border border-line bg-mantle p-1">
<div class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay">
<div
class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay"
>
<span class="text-green">&gt;</span> Windows (PowerShell)
</div>
<div class="relative">
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{psScript}</code></pre>
<pre
class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{psScript}</code></pre>
<CopyButton text={psScript} class="absolute right-2 top-2" />
</div>
</div>
<p class="mt-2 text-xs text-overlay">Downloads the latest release and adds <span class="text-text">termscp</span> to your user PATH.</p>
<p class="mt-2 text-xs text-overlay">
Downloads the latest release and adds <span class="text-text"
>termscp</span
> to your user PATH.
</p>
<h2 class="mt-10 mb-3 text-lg text-text">Package managers</h2>
<InstallTabs methods={methods} />
<p class="mt-8 text-sm text-overlay">Update anytime with <span class="text-green">termscp --update</span>.</p>
<p class="mt-8 text-sm text-overlay">
Update anytime with <span class="text-green">termscp --update</span>.
</p>
<p class="mt-2 text-sm text-overlay">
Linux build deps: <span class="text-text">libdbus-1</span>, <span class="text-text">pkg-config</span>, <span class="text-text">libsmbclient</span>.
More details in the <a href={DOCS_URL} class="text-blue hover:underline">docs</a>.
Linux build deps: <span class="text-text">libdbus-1</span>, <span
class="text-text">pkg-config</span
>, <span class="text-text">libsmbclient</span>. More details in the <a
href={DOCS_URL}
class="text-blue hover:underline">docs</a
>.
</p>
</main>
<Footer />

View File

@@ -80,9 +80,15 @@ html {
}
/* Theme toggle icons: shown based on active theme, set before paint by the head bootstrap */
[data-icon] { display: none; }
[data-theme="frappe"] [data-icon="frappe"] { display: inline; }
[data-theme="latte"] [data-icon="latte"] { display: inline; }
[data-icon] {
display: none;
}
[data-theme="frappe"] [data-icon="frappe"] {
display: inline;
}
[data-theme="latte"] [data-icon="latte"] {
display: inline;
}
.blink {
display: inline-block;
@@ -92,4 +98,8 @@ html {
vertical-align: -1px;
animation: blink 1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
@keyframes blink {
50% {
opacity: 0;
}
}

View File

@@ -2,25 +2,50 @@
"$schema": "https://openapi.vercel.sh/vercel.json",
"redirects": [
{ "source": "/index.html", "destination": "/", "permanent": true },
{ "source": "/get-started.html", "destination": "/install", "permanent": true },
{
"source": "/get-started.html",
"destination": "/install",
"permanent": true
},
{ "source": "/get-started", "destination": "/install", "permanent": true },
{ "source": "/updates.html", "destination": "/install", "permanent": true },
{ "source": "/user-manual.html", "destination": "https://docs.termscp.rs", "permanent": true },
{ "source": "/user-manual", "destination": "https://docs.termscp.rs", "permanent": true },
{ "source": "/changelog.html", "destination": "https://github.com/veeso/termscp/blob/main/CHANGELOG.md", "permanent": true }
{
"source": "/user-manual.html",
"destination": "https://docs.termscp.rs",
"permanent": true
},
{
"source": "/user-manual",
"destination": "https://docs.termscp.rs",
"permanent": true
},
{
"source": "/changelog.html",
"destination": "https://github.com/veeso/termscp/blob/main/CHANGELOG.md",
"permanent": true
}
],
"headers": [
{
"source": "/fonts/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/install.sh",
"headers": [{ "key": "Content-Type", "value": "text/x-shellscript; charset=utf-8" }]
"headers": [
{ "key": "Content-Type", "value": "text/x-shellscript; charset=utf-8" }
]
},
{
"source": "/install.ps1",
"headers": [{ "key": "Content-Type", "value": "text/plain; charset=utf-8" }]
"headers": [
{ "key": "Content-Type", "value": "text/plain; charset=utf-8" }
]
}
]
}