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
2 changes: 1 addition & 1 deletion resources/js/components/landing/agents-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function AgentsMcp() {
label="Agents"
headline="Let Claude query your database."
headlineMuted="Without giving it your password."
lede="TablePro runs an MCP server with sixteen tools over your saved connections. The client receives a scoped token, never a credential, and every call lands in an activity log kept for ninety days."
lede="An MCP server with sixteen tools over your saved connections. The client gets a scoped token, never a credential, and every call is logged."
>
<FullLine />
<Container>
Expand Down
29 changes: 16 additions & 13 deletions resources/js/components/landing/compare-table.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Container from '@/components/ui/container';
import DataTable, { TABLE_COLUMN_RULE, TABLE_ROW_RULE } from '@/components/ui/data-table';
import { FullLine } from '@/components/ui/full-line';
import SectionShell from '@/components/ui/section-shell';
import { getComparisonBySlug, type ComparisonInfo } from '@/data/comparisons';

/**
Expand All @@ -12,6 +11,13 @@ import { getComparisonBySlug, type ComparisonInfo } from '@/data/comparisons';
*
* Ordered so the runtime contrast reads first and the like-for-like comparison
* closes it.
*
* Headless since the merge. This was a section of its own directly below
* Migration, listing the same competitor set for a different question — two
* full header stacks, eyebrow through lede plus four rules apiece, for one
* argument. `switch-from.tsx` owns the shell now and renders this after its
* import grid. The `#compare` anchor moved onto the H3, so nothing that
* pointed at it breaks.
*/
const SLUGS = ['dbeaver', 'datagrip', 'beekeeper-studio', 'tableplus'] as const;

Expand Down Expand Up @@ -71,14 +77,12 @@ export default function CompareTable() {
}

return (
<SectionShell
tier="reference"
tone="raised"
id="compare"
label="Comparison"
headline="TablePro, DBeaver, DataGrip, Beekeeper Studio and TablePlus."
headlineMuted="Startup, memory, runtime and price."
>
<>
<Container>
<h3 id="compare" className="scroll-mt-20 px-4 py-4 text-lg font-semibold">
TablePro against DBeaver, DataGrip, Beekeeper Studio and TablePlus.
</h3>
</Container>
<FullLine />
<Container>
{/*
Expand Down Expand Up @@ -149,12 +153,11 @@ export default function CompareTable() {

<Container>
<p className="px-4 py-3 font-mono text-xs text-muted-foreground">
Every competitor figure comes from its own comparison page, which is where the version tested is
written down. TablePlus is native like TablePro, and its cold start and idle memory are not
measured here rather than estimated.
Each competitor figure comes from its own comparison page, which names the version tested.
TablePlus is native too: unmeasured here rather than estimated.
</p>
</Container>
<FullLine />
</SectionShell>
</>
);
}
6 changes: 3 additions & 3 deletions resources/js/components/landing/database-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default function DatabaseGrid() {
label="Databases"
headline="25 databases."
headlineMuted="One native driver each. No JDBC."
lede="MySQL, PostgreSQL, MongoDB, Redis and 21 more. Nine drivers ship inside the app; the rest download the first time you pick one, checksum and signature verified, with no restart."
lede="Nine drivers ship inside the app. The other sixteen download the first time you pick one, signature verified, with no restart."
>
<FullLine />
<Container>
Expand Down Expand Up @@ -334,10 +334,10 @@ export default function DatabaseGrid() {
<Ledger>
<LedgerRow label="Drivers">
libpq, libmariadb, hiredis, libmongoc, libcassandra, FreeTDS, OracleNIO. Teradata and Trino
speak their wire protocols in pure Swift. No JDBC jar, no ODBC bridge, anywhere.
speak their wire protocols in pure Swift.
</LedgerRow>
<LedgerRow label="Plugins">
Each driver is a signed bundle that loads over a stable ABI, so an app update never breaks one.
A signed bundle over a stable ABI, so an app update never breaks a driver.
</LedgerRow>
</Ledger>
</Container>
Expand Down
11 changes: 5 additions & 6 deletions resources/js/components/landing/footer-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default function FooterCTA() {
</div>

<p className="mt-4 text-sm text-muted-foreground">
With no connection to hand,{' '}
No database to hand?{' '}
<span className="font-mono text-xs">File &gt; Try Sample Database</span> opens a bundled
Chinook SQLite.
</p>
Expand All @@ -142,13 +142,12 @@ export default function FooterCTA() {
<div id="mobile" className="scroll-mt-20">
<h3 className={PANEL_TITLE}>iPhone and iPad</h3>
<p className="mt-2 text-sm text-muted-foreground">
Seven engines on device: SQLite, DuckDB, MySQL, MariaDB, PostgreSQL, Redis and SQL
Server, with SSH tunnels, Face ID and Handoff. Connections, groups and tags sync from
the Mac; syncing passwords is a separate opt in.
SQLite, DuckDB, MySQL, MariaDB, PostgreSQL, Redis and SQL Server on device, with SSH
tunnels, Face ID and Handoff.
</p>
<p className="mt-3 text-sm text-muted-foreground">
The app is free; syncing needs a Starter or Team license. Mac only: the plugin
registry, AI chat, the MCP server and ER diagrams.
Free, like the Mac app. Syncing needs a license. Mac only: plugins, AI chat, MCP and ER
diagrams.
</p>

<div className="mt-6">
Expand Down
154 changes: 154 additions & 0 deletions resources/js/components/landing/license.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import Container from '@/components/ui/container';
import DataTable, { TABLE_COLUMN_RULE, TABLE_ROW_RULE } from '@/components/ui/data-table';
import { FullLine } from '@/components/ui/full-line';
import { Availability } from '@/components/ui/glyph';
import SectionShell from '@/components/ui/section-shell';
import { PAID_FEATURES } from '@/data/license';

interface Row {
name: string;
/** Optional: the two terms rows carry their answer in the columns instead. */
detail?: string;
free: boolean | string;
starter: boolean | string;
team: boolean | string;
}

/**
* The paid surface, as one artifact, one section before the prices.
*
* It replaces the six-row "Compare plans" table that used to close Pricing.
* That table was the last thing on the section and named four paid features;
* this one names all nine, arrives while the reader is still deciding rather
* than after they have scrolled past the cards, and lets the pricing lede stop
* enumerating features in prose — it was 59 words, the longest paragraph on the
* page, and it was wrong about the count.
*
* A matrix rather than a grid of cards on purpose: the question a reader has
* here is "what do I lose by not paying", and three columns of ticks answer it
* without being read.
*/
function buildRows(teamMinSeats: number): Row[] {
return [
{
name: 'The whole app',
detail: '25 databases, SQL editor, data grid, AI assistant, MCP server, Safe Mode, SSH tunnels',
free: true,
starter: true,
team: true,
},
...PAID_FEATURES.map(
(feature): Row => ({
name: feature.name,
detail: feature.detail,
free: false,
starter: feature.tier === 'starter',
team: true,
}),
),
{ name: 'Mac activations', free: 'No license needed', starter: '2 Macs', team: `${teamMinSeats} seats minimum` },
{ name: 'Priority support', free: false, starter: false, team: true },
];
}

const HEAD_CELL = 'p-4 text-center text-sm font-medium sm:p-5';
const VALUE_CELL = 'p-4 text-center sm:p-5';

function Cell({ value }: { value: boolean | string }) {
if (typeof value === 'string') {
return <span className="text-sm font-medium text-foreground">{value}</span>;
}

return (
<span className="inline-flex justify-center">
<Availability included={value} />
</span>
);
}

export default function License({ teamMinSeats }: { teamMinSeats: number }) {
const rows = buildRows(teamMinSeats);

return (
<SectionShell
id="license"
label="License"
headline={`${PAID_FEATURES.length} features need a license.`}
headlineMuted="Everything else is free."
lede="Free is not a trial and not a demo. It is the whole app, on every Mac you own, with nothing counting down."
>
<FullLine />
<Container>
{/*
* The scrolling-table pattern from safety.tsx and
* compare-table.tsx: focusable and labelled because it scrolls,
* with FullLine kept outside so its 200vw bleed cannot add
* scroll width in here.
*/}
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="What each plan includes">
<DataTable
className="min-w-[40rem]"
caption="Every feature that needs a license, compared across Free, Starter and Team."
>
<thead>
<tr>
<th scope="col" className="p-4 text-left text-sm font-medium text-muted-foreground sm:p-5">
Feature
</th>
<th
scope="col"
className={`border-l ${TABLE_COLUMN_RULE} ${HEAD_CELL} text-muted-foreground`}
>
Free
</th>
<th
scope="col"
className={`border-l ${TABLE_COLUMN_RULE} bg-primary/5 ${HEAD_CELL} font-bold text-primary-strong`}
>
Starter
</th>
<th
scope="col"
className={`border-l ${TABLE_COLUMN_RULE} ${HEAD_CELL} text-muted-foreground`}
>
Team
</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.name} className={`border-t ${TABLE_ROW_RULE}`}>
<th scope="row" className="p-4 text-left align-top font-normal sm:p-5">
<span className="block text-sm font-medium text-foreground">{row.name}</span>
{row.detail && (
<span className="mt-1 block text-xs text-muted-foreground">
{row.detail}
</span>
)}
</th>
<td className={`border-l ${TABLE_COLUMN_RULE} ${VALUE_CELL}`}>
<Cell value={row.free} />
</td>
<td className={`border-l ${TABLE_COLUMN_RULE} bg-primary/5 ${VALUE_CELL}`}>
<Cell value={row.starter} />
</td>
<td className={`border-l ${TABLE_COLUMN_RULE} ${VALUE_CELL}`}>
<Cell value={row.team} />
</td>
</tr>
))}
</tbody>
</DataTable>
</div>
</Container>
<FullLine />

<Container>
<p className="px-4 py-3 font-mono text-xs text-muted-foreground">
A license verifies offline on every launch. A 30-day grace period covers a server we cannot reach.
</p>
</Container>
<FullLine />
</SectionShell>
);
}
Loading
Loading