Skip to content
Merged
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
106 changes: 106 additions & 0 deletions apps/labfox/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
:root {
color-scheme: dark;
--ground: #100b06;
--raised: #191009;
--raised-2: #22160c;
--porcelain: #f7f0e6;
--mist: #b8a892;
--line: rgb(247 240 230 / 14%);
--accent: #f97316;
--accent-2: #fbbf24;
--accent-soft: rgb(249 115 22 / 16%);
--ok: #62d59d;
--run: #fbbf24;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; background: var(--ground); }
body { margin: 0; overflow-x: hidden; background: var(--ground); color: var(--porcelain); font-family: var(--font-sans); }
a { color: inherit; text-underline-offset: 4px; }
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

.shell { width: min(1140px, calc(100% - 3rem)); margin: auto; }
.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 20; padding: .7rem 1rem; background: var(--accent); color: #1a1006; font-weight: 700; text-decoration: none; }
.skip-link:focus { top: 1rem; }

.eyebrow { margin: 0; color: var(--accent); font: 800 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; }
.wordmark { font: 800 1rem var(--font-mono); letter-spacing: .12em; text-decoration: none; }
.wordmark span { color: var(--accent); }

.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; border: 1px solid var(--line); padding: .7rem 1.15rem; color: var(--porcelain); font: 800 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; border-radius: 10px; }
.button:hover { border-color: rgb(247 240 230 / 42%); }
.button.primary { border-color: var(--accent); background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #1a1006; }
.button.ghost { min-height: 40px; padding: .5rem .8rem; }

.site-header { position: relative; z-index: 4; min-height: 74px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
.site-header nav { display: flex; align-items: center; gap: .5rem; }
.site-header nav a:not(.button) { min-height: 44px; display: grid; place-items: center; padding: 0 .6rem; font: 700 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; color: var(--mist); }
.site-header nav a:not(.button):hover { color: var(--porcelain); }

.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); background: radial-gradient(circle at 82% 12%, rgb(249 115 22 / 22%), transparent 42%), radial-gradient(circle at 96% 84%, rgb(251 191 36 / 12%), transparent 40%), var(--ground); }
.hero__grid { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(20rem, .82fr); gap: clamp(2rem, 6vw, 5rem); align-items: center; min-height: min(680px, calc(100vh - 74px)); }
.hero__copy { padding: clamp(4.5rem, 9vw, 8rem) 0; }
h1 { max-width: 12ch; margin: 1.2rem 0 1.5rem; font-size: clamp(3.4rem, 7vw, 6.4rem); line-height: .9; letter-spacing: -.06em; }
h1 em, h2 em { font-family: Georgia, serif; font-weight: 400; font-style: italic; color: var(--accent-2); }
.lede { max-width: 40rem; margin: 0; color: var(--mist); font-size: clamp(1.05rem, 1.5vw, 1.22rem); line-height: 1.65; }
.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2.2rem; }
.hero__meta { margin: 1.75rem 0 0; color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .1em; }

.hero__panel { position: relative; justify-self: end; width: min(100%, 30rem); padding: 1.25rem; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(180deg, var(--raised-2), var(--raised)); box-shadow: 0 30px 90px rgb(0 0 0 / 45%); }
.hero__panel-bar { display: flex; gap: .5rem; margin-bottom: 1.25rem; }
.hero__panel-bar span { width: 11px; height: 11px; border-radius: 50%; background: rgb(247 240 230 / 20%); }
.hero__panel-bar span:first-child { background: var(--accent); }
.hero__mr-label { margin: 0 0 .4rem; color: var(--accent); font: 700 .66rem var(--font-mono); letter-spacing: .08em; }
.hero__mr h2 { margin: 0; font-size: 1.5rem; line-height: 1.1; letter-spacing: -.02em; }
.hero__mr-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: 1rem; color: var(--mist); font: 700 .66rem var(--font-mono); }
.pill { padding: .28rem .55rem; border-radius: 999px; font: 800 .62rem var(--font-mono); letter-spacing: .04em; }
.pill--open { background: var(--accent-soft); color: var(--accent-2); }
.pill--pipe { background: rgb(98 213 157 / 16%); color: var(--ok); }
.hero__pipeline { display: flex; align-items: center; gap: .5rem; margin-top: 1.25rem; padding-top: 1.1rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .06em; }
.dot { width: 9px; height: 9px; border-radius: 50%; }
.dot--ok { background: var(--ok); }
.dot--run { background: var(--run); box-shadow: 0 0 0 4px rgb(251 191 36 / 18%); }

.section-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 1.5rem 3rem; align-items: end; }
.section-intro .eyebrow { grid-column: 1 / -1; }
h2 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.6rem); line-height: .94; letter-spacing: -.06em; }
.section-intro > p:last-child { margin: 0; color: var(--mist); line-height: 1.65; align-self: end; }

.features { padding: clamp(5rem, 10vw, 9rem) 0; }
.feature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 3.5rem; border: 1px solid var(--line); background: var(--line); }
.feature { position: relative; padding: 2rem 1.75rem 2.25rem; background: var(--raised); transition: background .25s ease; }
.feature:hover { background: var(--raised-2); }
.feature__num { color: var(--accent); font: 800 .8rem var(--font-mono); letter-spacing: .08em; }
.feature h3 { margin: 1.5rem 0 .8rem; font-size: 1.5rem; line-height: 1.05; letter-spacing: -.02em; }
.feature p { margin: 0; color: var(--mist); font-size: .98rem; line-height: 1.6; }

.platforms { padding: 0 0 clamp(5rem, 10vw, 9rem); }
.platform-rail { display: flex; flex-wrap: wrap; gap: 1px; margin: 3rem 0 0; padding: 0; list-style: none; border: 1px solid var(--line); background: var(--line); }
.platform-rail li { flex: 1 1 8rem; padding: 1.6rem 1.25rem; background: var(--raised); font: 800 1.1rem var(--font-sans); letter-spacing: -.01em; }
.platform-rail li::before { content: "→ "; color: var(--accent); }

.close { padding: clamp(5rem, 10vw, 8rem) 0; border-top: 1px solid var(--line); background: radial-gradient(circle at 12% 20%, rgb(249 115 22 / 14%), transparent 42%), var(--ground); }
.close__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2rem; align-items: center; }
.close h2 { max-width: 12ch; }
.close__grid > div { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
.close__grid p { margin: 0; color: var(--mist); font: 700 .7rem var(--font-mono); letter-spacing: .06em; }

.site-footer { border-top: 1px solid var(--line); padding: clamp(3rem, 6vw, 5rem) 0 2rem; }
.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 2.5rem; }
.site-footer__grid > div:first-child p { max-width: 26rem; color: var(--mist); line-height: 1.6; }
.site-footer nav { display: flex; gap: 3rem; }
.site-footer nav div { display: flex; flex-direction: column; gap: .55rem; }
.site-footer nav strong { margin-bottom: .3rem; font: 800 .7rem var(--font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--porcelain); }
.site-footer nav a { color: var(--mist); font-size: .92rem; text-decoration: none; }
.site-footer nav a:hover { color: var(--porcelain); }
.site-footer__legal { display: flex; justify-content: space-between; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .66rem var(--font-mono); letter-spacing: .06em; }

@media (max-width: 900px) {
.hero__grid { grid-template-columns: 1fr; }
.hero__panel { justify-self: stretch; margin-bottom: 4rem; }
.section-intro { grid-template-columns: 1fr; }
.feature-grid, .close__grid, .site-footer__grid { grid-template-columns: 1fr; }
.site-footer nav { gap: 2rem; }
.site-footer__legal { flex-direction: column; gap: .5rem; }
}
13 changes: 13 additions & 0 deletions apps/labfox/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions apps/labfox/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
import "@theorvane/design-tokens/tokens.css";
import "./globals.css";

const siteUrl = new URL("https://labfox.theorvane.tech");
const title = "LabFox — the GitLab workflow client for every platform";
const description = "LabFox is a cross-platform GitLab client for merge requests, code review, and CI/CD — on Android, iOS, Windows, and macOS, with first-class self-hosted support.";

export const metadata: Metadata = {
metadataBase: siteUrl,
title,
description,
applicationName: "LabFox",
keywords: ["LabFox", "GitLab", "GitLab client", "merge requests", "code review", "CI/CD", "self-hosted", "Flutter", "cross-platform"],
authors: [{ name: "Theorvane", url: "https://theorvane.tech" }],
creator: "Theorvane",
publisher: "Theorvane",
category: "technology",
alternates: { canonical: "/" },
robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1, "max-video-preview": -1 } },
openGraph: { type: "website", url: siteUrl, title, description, siteName: "LabFox", locale: "en_US" },
twitter: { card: "summary_large_image", title, description },
};

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
{/* Analytics: add the Rybbit script here once a LabFox site id is provisioned. */}
<body>{children}</body>
</html>
);
}
55 changes: 55 additions & 0 deletions apps/labfox/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ImageResponse } from "next/og";

export const alt = "LabFox — the GitLab workflow client for every platform";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

// The site's own warm dark palette, echoing the amber accent on the landing page.
const ink = "#f7f0e6";
const ground = "#100b06";
const glow = "#2a1608";
const accent = "#f97316";
const muted = "#b8a892";

export default function OpenGraphImage() {
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "72px 80px",
background: `radial-gradient(circle at 82% 8%, ${glow} 0%, ${ground} 55%)`,
color: ink,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 18, fontSize: 34, fontWeight: 800, letterSpacing: "0.08em" }}>
<div style={{ width: 38, height: 38, borderRadius: 10, background: accent }} />
<div style={{ display: "flex" }}>LABFOX</div>
</div>

<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<div style={{ display: "flex", fontSize: 27, letterSpacing: "0.1em", color: accent, textTransform: "uppercase" }}>
GitLab workflow client
</div>
<div style={{ display: "flex", flexDirection: "column", fontSize: 84, fontWeight: 700, letterSpacing: "-0.045em", lineHeight: 1.02 }}>
<div style={{ display: "flex" }}>Your GitLab,</div>
<div style={{ display: "flex", color: accent }}>in hand.</div>
</div>
<div style={{ display: "flex", fontSize: 30, lineHeight: 1.45, color: muted, maxWidth: 940 }}>
Merge requests, code review, and pipelines — native on every platform, self-hosted included.
</div>
</div>

<div style={{ display: "flex", justifyContent: "space-between", fontSize: 26, color: muted }}>
<div style={{ display: "flex" }}>labfox.theorvane.tech</div>
<div style={{ display: "flex" }}>Android · iOS · Windows · macOS</div>
</div>
</div>
),
size,
);
}
92 changes: 92 additions & 0 deletions apps/labfox/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ExternalLink, SkipLink } from "@theorvane/ui";

const github = "https://github.com/Theorvane/labfox";
const studio = "https://theorvane.tech/";
const siteUrl = "https://labfox.theorvane.tech/";

const features = [
{
number: "01",
title: "Merge requests, reviewed in full",
body: "Open, filter, and act on merge requests with threaded discussion, diffs, and approvals — the whole review loop, not a read-only mirror.",
},
{
number: "02",
title: "Pipelines you can actually watch",
body: "Follow CI/CD jobs as they run, read logs inline, and retry or cancel a stage without leaving the request you were reviewing.",
},
{
number: "03",
title: "Self-hosted is first-class",
body: "Point LabFox at gitlab.com or your own instance. Personal and project tokens stay on the device; nothing is proxied through us.",
},
{
number: "04",
title: "One client, every platform",
body: "A single Flutter codebase ships a native-feeling app for Android, iOS, Windows, and macOS — the same workflow wherever you work.",
},
] as const;

const platforms = ["Android", "iOS", "Windows", "macOS"] as const;

const schema = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "LabFox",
url: siteUrl,
applicationCategory: "DeveloperApplication",
operatingSystem: "Android, iOS, Windows, macOS",
description: "A cross-platform GitLab workflow client for merge requests, code review, and CI/CD, with first-class self-hosted support.",
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
publisher: { "@type": "Organization", name: "Theorvane", url: studio },
};

export default function HomePage() {
return <>
<SkipLink />
<script data-testid="product-schema" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />
<header className="site-header shell">
<a className="wordmark" href="#top">LAB<span>FOX</span></a>
<nav aria-label="Primary"><a href="#features">Features</a><a href="#platforms">Platforms</a><ExternalLink href={studio}>Theorvane ↗</ExternalLink><ExternalLink className="button ghost" href={github}>GitHub ↗</ExternalLink></nav>
</header>
<main id="main-content">
<section className="hero" data-testid="labfox-hero" id="top">
<div className="shell hero__grid">
<div className="hero__copy">
<p className="eyebrow">GitLab workflow client · cross-platform</p>
<h1>Your GitLab, <em>in hand.</em></h1>
<p className="lede">LabFox brings merge requests, code review, and pipelines to a fast, native app on every platform you carry — with self-hosted instances treated as first-class, not an afterthought.</p>
<div className="actions"><ExternalLink className="button primary" href={github}>Get LabFox ↗</ExternalLink><a className="button" href="#features">See what it does</a></div>
<p className="hero__meta">Android · iOS · Windows · macOS · self-hosted</p>
</div>
<div aria-hidden="true" className="hero__panel">
<div className="hero__panel-bar"><span /><span /><span /></div>
<div className="hero__mr"><p className="hero__mr-label">Merge request · !482</p><h2>Gate risky applies behind review</h2><div className="hero__mr-meta"><span className="pill pill--open">Open</span><span className="pill pill--pipe">Pipeline passed</span><span>3 approvals</span></div></div>
<div className="hero__pipeline"><span className="dot dot--ok" />build<span className="dot dot--ok" />test<span className="dot dot--run" />deploy</div>
</div>
</div>
</section>

<section className="features shell" id="features">
<div className="section-intro"><p className="eyebrow">What it does / 01</p><h2>The whole review loop,<br /><em>on your device.</em></h2><p>LabFox is a client, not a dashboard. Every action you take on the web — approve, comment, retry a job — you take here.</p></div>
<div className="feature-grid">
{features.map((feature) => (
<article className="feature" key={feature.number}>
<span className="feature__num">{feature.number}</span>
<h3>{feature.title}</h3>
<p>{feature.body}</p>
</article>
))}
</div>
</section>

<section className="platforms shell" id="platforms">
<div className="section-intro"><p className="eyebrow">Everywhere / 02</p><h2>One workflow.<br /><em>Every platform.</em></h2><p>Built on a single Flutter codebase so the app you learn on your phone is the app you use at your desk.</p></div>
<ul className="platform-rail" aria-label="Supported platforms">{platforms.map((name) => <li key={name}>{name}</li>)}</ul>
</section>

<section className="close"><div className="shell close__grid"><p className="eyebrow">Open source · MIT</p><h2>Bring GitLab with you.</h2><div><ExternalLink className="button primary" href={github}>Get LabFox ↗</ExternalLink><p>A Theorvane product · your tokens stay yours</p></div></div></section>
</main>
<footer className="site-footer"><div className="shell site-footer__grid"><div><a className="wordmark" href="#top">LAB<span>FOX</span></a><p>A cross-platform GitLab workflow client from Theorvane.</p></div><nav aria-label="Footer"><div><strong>LabFox</strong><a href="#features">Features</a><a href="#platforms">Platforms</a><ExternalLink href={github}>Source ↗</ExternalLink></div><div><strong>Studio</strong><ExternalLink href={studio}>Theorvane ↗</ExternalLink><ExternalLink href="https://github.com/Theorvane">GitHub ↗</ExternalLink></div></nav></div><div className="shell site-footer__legal"><span>© {new Date().getFullYear()} Theorvane.</span><span>Built with intent.</span></div></footer>
</>;
}
11 changes: 11 additions & 0 deletions apps/labfox/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MetadataRoute } from "next";

const siteUrl = "https://labfox.theorvane.tech";

export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: `${siteUrl}/sitemap.xml`,
host: siteUrl,
};
}
9 changes: 9 additions & 0 deletions apps/labfox/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { MetadataRoute } from "next";

const siteUrl = "https://labfox.theorvane.tech";
// Bump when published page content changes. A build timestamp would report freshness that did not happen.
const lastModified = "2026-08-28";

export default function sitemap(): MetadataRoute.Sitemap {
return [{ url: `${siteUrl}/`, lastModified, changeFrequency: "monthly", priority: 1 }];
}
6 changes: 6 additions & 0 deletions apps/labfox/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
5 changes: 5 additions & 0 deletions apps/labfox/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = { turbopack: { root: "../.." } };

export default nextConfig;
8 changes: 8 additions & 0 deletions apps/labfox/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@theorvane/labfox-site",
"version": "0.1.0",
"private": true,
"scripts": { "dev": "next dev", "build": "next build", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run" },
"dependencies": { "@theorvane/design-tokens": "*", "@theorvane/ui": "*", "next": "^16.2.1", "react": "^19.2.0", "react-dom": "^19.2.0" },
"devDependencies": { "@testing-library/react": "^16.3.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "jsdom": "^28.1.0", "vitest": "^4.1.0" }
}
Loading
Loading