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
19 changes: 19 additions & 0 deletions resources/js/components/landing/agents-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Container from '@/components/ui/container';
import { FullLine } from '@/components/ui/full-line';
import { Ledger, LedgerRow } from '@/components/ui/ledger';
import SectionShell from '@/components/ui/section-shell';
import { relocatedFaq } from '@/data/home-faqs';
import { cellBorders, type ColumnMap } from '@/components/ui/grid-cell';

/** Kept byte-identical to the tokenized block below, since this is what gets copied. */
Expand Down Expand Up @@ -112,6 +113,8 @@ function CopyButton() {
);
}

const aiSafetyFaq = relocatedFaq('Can the AI drop a table on production?');

/**
* No screenshot exists for the MCP server, so this section stays typographic:
* the config you paste on the left, the permission ladder on the right.
Expand Down Expand Up @@ -215,6 +218,22 @@ export default function AgentsMcp() {
</Container>
<FullLine />

{/*
* This section raises the most alarming claim on the page, so it
* carries the first half of the answer rather than deferring all of
* it to the FAQ. Verbatim from relocatedFaqs — one copy of the
* answer, and /faq still lists it.
*/}
<Container>
<div className="border-l-2 border-primary p-4 sm:p-6">
<h3 className="text-base font-semibold text-foreground">{aiSafetyFaq.question}</h3>
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted-foreground">
{aiSafetyFaq.answer}
</p>
</div>
</Container>
<FullLine />

<Container>
<p className="p-4 font-mono text-xs text-muted-foreground">
Remote access is off by default. Turning it on switches authentication and TLS on automatically,
Expand Down
61 changes: 13 additions & 48 deletions resources/js/components/landing/depth-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,35 @@ import { FullLine } from '@/components/ui/full-line';
import SectionShell from '@/components/ui/section-shell';
import { cellBorders, GridCell, type ColumnMap } from '@/components/ui/grid-cell';

/** 12 items divide evenly into 1, 2 and 3 columns, so no filler cells are needed. */
/** 6 items divide evenly into 1, 2 and 3 columns, so no filler cells are needed. */
const COLS: ColumnMap = { base: 1, sm: 2, lg: 3 };

interface DepthItem {
title: string;
/** One sentence. A node only where the copy carries an inline mono token. */
body: ReactNode;
/** Rendered mono already, so shortcuts and identifiers stay plain text. */
spec: string;
}

function Mono({ children }: { children: ReactNode }) {
return <span className="font-mono text-[0.9em]">{children}</span>;
}

const ITEMS: DepthItem[] = [
{
title: 'ER diagram',
body: 'Automatic two-dimensional layout that groups tables by foreign key instead of stacking one tall column.',
spec: "crow's foot from PKs and unique indexes · junction tables collapse · export PNG or CREATE TABLE",
},
{
title: 'EXPLAIN, visualized',
body: 'Three views of the plan: a cost-coloured diagram, an expandable tree, and the raw text.',
spec: '⌘⌥E · ClickHouse Plan/Pipeline/AST · Trino Logical/Distributed/IO · BigQuery dry-run cost',
},
{
title: 'Server dashboard',
body: 'Active sessions, per-engine metrics and slow queries, with Cancel Query and Terminate Session behind a confirmation.',
spec: 'pg_cancel_backend and KILL QUERY · slow = over 1s · refresh 1s to 30s, default 5s',
},
{
title: 'Users and roles',
body: 'Grant and revoke without hand-writing GRANT, from the server down to a single column.',
spec: 'MySQL, MariaDB, PostgreSQL, PGlite · staged as a diff · PostgreSQL applies in one transaction',
},
{
title: 'Vim mode',
body: (
<>
Six modes with the full motion and operator set, so <Mono>:w</Mono> runs the query.
</>
),
spec: 'counts, text objects, registers "a to "z, marks, macros capped at 50 recursions',
},
{
title: 'Query history',
body: 'Every query you run is saved, successful or not, and searchable.',
spec: 'local SQLite with an FTS5 index · 10,000 entries, 90 days, both configurable · ⌘Y',
},
{
title: 'Quick Switcher',
body: 'Fuzzy search across tables, databases, saved queries and history, ranked by frecency.',
spec: '⌘⇧O · four scopes on ⌘1 to ⌘4 · ⌘K switches database, and says Keyspace on Cassandra',
},
{
title: 'CSV inspector',
Expand All @@ -67,37 +42,30 @@ const ITEMS: DepthItem[] = [
step.
</>
),
spec: 'detects delimiter, encoding and line ending · split by regex, merge columns · saves byte-faithfully',
},
{
title: 'Import and export',
body: 'Five export formats, streaming at constant memory with no row limit, written atomically.',
spec: 'CSV, JSON, SQL, MQL, XLSX · imports SQL, JSON, CSV with rollback, commit or skip',
},
{
title: 'Connection import',
body: 'Bring your connections from six other clients, passwords and tunnels included.',
spec: 'TablePlus, Sequel Ace, DBeaver, DataGrip, Beekeeper Studio, Navicat · source app need not be running',
},
{
title: 'Backup and restore',
body: "PostgreSQL and Redshift dumps through the connection's existing SSH tunnel.",
spec: 'pg_dump -Fc · live byte counter · restore runs --no-owner --no-acl',
},
{
title: 'Plugins and themes',
body: 'Drivers install when you pick them and update without a restart.',
spec: 'SHA-256 and code signature checked before load · staged updates · four built-in themes, more from the registry',
},
];

/**
* The long tail, kept to one flush grid so it reads as an index rather than
* twelve more pitches. The mono ordinal is the only accent in each cell.
* six more pitches. The mono ordinal is the only accent in each cell.
*
* Six, not twelve. Three of the originals repeated a section above — Vim mode
* from the workbench ledger, connection import from the whole SwitchFrom
* section, plugin verification from the database grid's own lede — and three
* were table stakes every competitor also ships. What is left is the set a
* reader could not assume.
*
* The `spec` line went with them. It carried shortcuts, thresholds and defaults
* — genuinely useful, genuinely documentation, and roughly 250 words standing
* between the reader and the next call to action.
*/
export default function DepthGrid() {
return (
<SectionShell tier="reference" id="more" label="Depth" headline="Twelve more things" headlineMuted="you would expect to pay for.">
<SectionShell tier="reference" id="more" label="Depth" headline="Six more things" headlineMuted="you would expect to pay for.">
<FullLine />
<Container>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
Expand All @@ -114,9 +82,6 @@ export default function DepthGrid() {
</p>
<h3 className="mt-3 text-base font-semibold">{item.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{item.body}</p>
<p className="mt-4 font-mono text-xs leading-relaxed break-words text-muted-foreground">
{item.spec}
</p>
</GridCell>
))}
</div>
Expand Down
8 changes: 3 additions & 5 deletions resources/js/components/landing/download-rail.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Button from '@/components/ui/button';
import Container from '@/components/ui/container';
import { FullLine } from '@/components/ui/full-line';

Expand Down Expand Up @@ -37,13 +38,10 @@ export default function DownloadRail({ note }: Props) {
<Container>
<div className="flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
<p className="font-mono text-xs text-muted-foreground">{note}</p>
<a
href="/download"
className="inline-flex shrink-0 items-center justify-center gap-2 rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
<Button href="/download" size="sm" className="shrink-0">
<AppleGlyph />
Download for Mac
</a>
</Button>
</div>
</Container>
<FullLine />
Expand Down
12 changes: 6 additions & 6 deletions resources/js/components/landing/footer-cta.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { toast } from 'sonner';
import { buttonClasses } from '@/components/ui/button';
import Container from '@/components/ui/container';
import { FullLine } from '@/components/ui/full-line';
import { cellBorders, GridCell, type ColumnMap } from '@/components/ui/grid-cell';
import SectionShell from '@/components/ui/section-shell';
import Button from '@/components/ui/button';

interface FlashMessage {
type: 'success' | 'warning' | 'error';
Expand Down Expand Up @@ -96,8 +98,7 @@ function useEmailForm(endpoint: string) {
*/
const INPUT_CLASS =
'min-w-0 flex-1 rounded-lg border border-rule bg-transparent px-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground disabled:opacity-50';
const SUBMIT_CLASS =
'shrink-0 rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50';
const SUBMIT_CLASS = buttonClasses('primary', 'sm', 'shrink-0 disabled:opacity-50');

export default function FooterCTA() {
const [showBeta, setShowBeta] = useState(false);
Expand Down Expand Up @@ -220,20 +221,19 @@ export default function FooterCTA() {
</p>

<div className="mt-6 flex flex-wrap items-center gap-3">
<a
<Button
href="/download"
className="inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
<AppleGlyph />
Download for Mac
</a>
</Button>
<button
type="button"
data-beta-toggle
aria-expanded={showBeta}
aria-controls="beta-invite"
onClick={() => setShowBeta(true)}
className="inline-flex items-center gap-2 rounded-full border border-rule px-6 py-3 text-sm font-semibold text-foreground transition-colors"
className={buttonClasses('secondary')}
>
Get for iPhone
</button>
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/landing/header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { buttonClasses } from '@/components/ui/button';
import MobileNav from './mobile-nav';

interface Props {
Expand Down Expand Up @@ -132,7 +133,7 @@ export default function Header({ githubStars }: Props) {
type="button"
onClick={toggleDropdown}
onKeyDown={handleTriggerKeyDown}
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90"
className={buttonClasses('primary', 'sm', 'px-4 py-2 font-medium')}
aria-expanded={downloadOpen}
aria-haspopup="menu"
>
Expand Down
15 changes: 7 additions & 8 deletions resources/js/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Container from '@/components/ui/container';
import { AccentLine, FullLine } from '@/components/ui/full-line';
import ThemedImage from '@/components/ui/themed-image';
import SectionLabel from '@/components/ui/section-label';
import Button from '@/components/ui/button';

interface Props {
githubStars?: number | null;
Expand Down Expand Up @@ -74,27 +75,25 @@ export default function Hero({ githubStars, latestRelease }: Props) {
</p>
<FullLine />
<div className="flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center">
<a
<Button
href="/download"
className="inline-flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
<AppleGlyph />
Download for Mac
</a>
<a
</Button>
<Button variant="secondary"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-full border border-rule px-6 py-3 text-sm font-semibold text-foreground transition-colors"
>
<Star className="size-4" strokeWidth={1.75} aria-hidden="true" />
<Star strokeWidth={1.75} aria-hidden="true" />
View source
{githubStars && githubStars > 0 ? (
<span className="font-mono text-xs text-muted-foreground">
<span>
{formatStarCount(githubStars)}
</span>
) : null}
</a>
</Button>
<a
href="#mobile"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
Expand Down
9 changes: 3 additions & 6 deletions resources/js/components/landing/mobile-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
import Button from '@/components/ui/button';

interface Props {
isOpen: boolean;
Expand Down Expand Up @@ -155,13 +156,9 @@ export default function MobileNav({ isOpen, onClose }: Props) {

<div className="my-1 h-px w-12 bg-foreground/10" />

<a
href="/download"
onClick={onClose}
className="inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
<Button href="/download" size="lg" onClick={onClose}>
Download for Mac
</a>
</Button>
<a
href="/#mobile"
onClick={onClose}
Expand Down
6 changes: 3 additions & 3 deletions resources/js/components/landing/mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GridCell } from '@/components/ui/grid-cell';
import { Ledger, LedgerRow } from '@/components/ui/ledger';
import SectionShell from '@/components/ui/section-shell';
import ThemedImage from '@/components/ui/themed-image';
import Button from '@/components/ui/button';

/**
* The iPhone frame is decorative chrome; the screenshot inside carries the alt
Expand Down Expand Up @@ -80,12 +81,11 @@ export default function Mobile() {

<Container>
<div className="px-4 py-4">
<a
<Button variant="secondary"
href="#footer-cta"
className="inline-flex items-center gap-2 rounded-full border border-rule px-6 py-3 text-sm font-semibold text-foreground transition-colors"
>
Join the iOS beta <span aria-hidden="true">&rarr;</span>
</a>
</Button>
</div>
</Container>
<FullLine />
Expand Down
52 changes: 52 additions & 0 deletions resources/js/components/landing/objection-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Container from '@/components/ui/container';
import { FullLine } from '@/components/ui/full-line';
import { cellBorders, type ColumnMap } from '@/components/ui/grid-cell';
import { relocatedFaq } from '@/data/home-faqs';

const COLS: ColumnMap = { base: 1, md: 2 };

const QUESTIONS = [
'Is it really free, or free for now?',
'Can I use TablePro at work under AGPLv3?',
] as const;

/**
* The two questions that stop a download, answered where they arise.
*
* Both used to sit in the FAQ at position fifteen, behind the entire spec
* sheet — which is to say behind roughly three thousand words, past the point
* where a reader holding either objection has already left. The AGPL one blocks
* the highest-value visitor of all: the person evaluating a Team licence for a
* company.
*
* Verbatim from `relocatedFaqs`, so there is one copy of each answer and /faq
* still lists both.
*
* No heading and no eyebrow: this is a row in the hero's own rhythm, not a
* section competing with it.
*/
export default function ObjectionRow() {
return (
<section aria-label="Common questions before downloading">
<FullLine />
<Container>
<div className="grid grid-cols-1 md:grid-cols-2">
{QUESTIONS.map((question, i) => {
const faq = relocatedFaq(question);

return (
<div
key={question}
className={`p-6 sm:p-8 ${cellBorders(i, COLS, QUESTIONS.length)} border-rule`}
>
<h2 className="text-base font-semibold text-foreground">{faq.question}</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{faq.answer}</p>
</div>
);
})}
</div>
</Container>
<FullLine />
</section>
);
}
Loading
Loading