diff --git a/resources/css/app.css b/resources/css/app.css index 352d534..e986823 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -44,7 +44,14 @@ --text-xs--line-height: 1.5; --text-xs--letter-spacing: 0.0005em; --text-sm: 0.875rem; - --text-sm--line-height: 1.55; + /* + * 1.625, not the 1.55 the curve suggests. Ninety-four elements overrode it + * with `leading-relaxed`, and the overrides were right: `text-sm` is the + * site's body copy rung, and 14px prose wants more air than a 14px table + * cell. A scale that every prose paragraph has to argue with is not a scale, + * so the scale changed rather than the ninety-four. + */ + --text-sm--line-height: 1.625; --text-sm--letter-spacing: -0.0062em; --text-base: 1rem; --text-base--line-height: 1.6; diff --git a/resources/js/components/landing/agents-mcp.tsx b/resources/js/components/landing/agents-mcp.tsx index 8dc35f0..fe78af9 100644 --- a/resources/js/components/landing/agents-mcp.tsx +++ b/resources/js/components/landing/agents-mcp.tsx @@ -1,13 +1,11 @@ -import { useEffect, useRef, useState } from 'react'; -import { Check, Copy } from 'lucide-react'; -import { toast } from 'sonner'; import { CodeBlock } from '@/components/ui/code'; +import CopyButton from '@/components/ui/copy-button'; 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'; +import { ITEM_TITLE, cellBorders, type ColumnMap } from '@/components/ui/grid-cell'; /** Kept byte-identical to the tokenized block below, since this is what gets copied. */ const CONFIG_JSON = `{ @@ -70,49 +68,6 @@ function Tool({ children }: { children: string }) { return {children}; } -function CopyButton() { - const [copied, setCopied] = useState(false); - const timeoutRef = useRef | null>(null); - - useEffect( - () => () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }, - [], - ); - - async function handleCopy() { - try { - await navigator.clipboard.writeText(CONFIG_JSON); - toast('Copied'); - setCopied(true); - - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => setCopied(false), 1200); - } catch { - toast.error('Could not copy. Select the text and copy it by hand.'); - } - } - - return ( - - ); -} const aiSafetyFaq = relocatedFaq('Can the AI drop a table on production?'); @@ -139,7 +94,7 @@ export default function AgentsMcp() { claude_desktop_config.json - + @@ -229,8 +184,8 @@ export default function AgentsMcp() { */}
-

{aiSafetyFaq.question}

-

+

{aiSafetyFaq.question}

+

{aiSafetyFaq.answer}

diff --git a/resources/js/components/landing/architecture.tsx b/resources/js/components/landing/architecture.tsx index bc3aee5..49c5831 100644 --- a/resources/js/components/landing/architecture.tsx +++ b/resources/js/components/landing/architecture.tsx @@ -69,7 +69,7 @@ export default function Architecture() { > -

+

Native macOS database clients come in three shapes today. Single database and open source, like Sequel Ace and Postico. Multi database and closed source, like TablePlus. Multi database but not native, like DBeaver on the JVM or Beekeeper Studio and DBGate on Electron.{' '} @@ -95,7 +95,7 @@ export default function Architecture() {

{material.label}

-

+

{material.body}

@@ -116,7 +116,7 @@ export default function Architecture() { className={`${CELL_DENSITY.default} ${cellBorders(i, COLS, BEHAVIOURS.length)} border-rule`} >

{behaviour.title}

-

+

{behaviour.body}

diff --git a/resources/js/components/landing/closing-cta.tsx b/resources/js/components/landing/closing-cta.tsx new file mode 100644 index 0000000..acc7746 --- /dev/null +++ b/resources/js/components/landing/closing-cta.tsx @@ -0,0 +1,49 @@ +import { ReactNode } from 'react'; +import Button from '@/components/ui/button'; +import Container from '@/components/ui/container'; +import { FullLine } from '@/components/ui/full-line'; +import { AppleGlyph } from '@/components/ui/glyph'; + +interface ClosingCtaProps { + /** + * Anything to place beside the download button — a secondary link, a second + * platform. Given one, the row goes horizontal from sm. + */ + children?: ReactNode; +} + +/** + * The block that closes a comparison or database page. + * + * It existed twice, differing only in one sentence and in whether its button + * row could hold a second control — so the two pages disagreed on the OS + * requirement line, which is the sentence a reader actually checks before + * downloading. It says the same thing on both now, and it says the thing every + * credible Mac app puts under its download button: the OS floor and the + * architectures. + */ +export default function ClosingCta({ children }: ClosingCtaProps) { + return ( + <> +
+ + + +
+

Try TablePro for free.

+

+ Free and open source. macOS 14+, Apple Silicon and Intel. No account. +

+
+ + {children} +
+
+ +
+ + ); +} diff --git a/resources/js/components/landing/database-grid.tsx b/resources/js/components/landing/database-grid.tsx index 4de2831..088941a 100644 --- a/resources/js/components/landing/database-grid.tsx +++ b/resources/js/components/landing/database-grid.tsx @@ -281,7 +281,7 @@ export default function DatabaseGrid() { >
diff --git a/resources/js/components/landing/footer-cta.tsx b/resources/js/components/landing/footer-cta.tsx index 791e88e..3f33b86 100644 --- a/resources/js/components/landing/footer-cta.tsx +++ b/resources/js/components/landing/footer-cta.tsx @@ -2,9 +2,10 @@ import { useEffect, useRef, useState } from 'react'; import { Check, Copy } from 'lucide-react'; import { toast } from 'sonner'; import { buttonClasses } from '@/components/ui/button'; +import CopyButton from '@/components/ui/copy-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 { PANEL_TITLE, cellBorders, GridCell, type ColumnMap } from '@/components/ui/grid-cell'; import SectionShell from '@/components/ui/section-shell'; import Button from '@/components/ui/button'; import { AppleGlyph } from '@/components/ui/glyph'; @@ -97,7 +98,6 @@ const SUBMIT_CLASS = buttonClasses('primary', 'sm', 'shrink-0 disabled:opacity-5 export default function FooterCTA() { const [showBeta, setShowBeta] = useState(false); const [subscriberCount, setSubscriberCount] = useState(null); - const [copied, setCopied] = useState(false); const copyTimeoutRef = useRef | null>(null); const statsAnchorRef = useRef(null); const betaForm = useEmailForm('/beta/signup'); @@ -149,14 +149,6 @@ export default function FooterCTA() { }; }, []); - useEffect( - () => () => { - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current); - } - }, - [], - ); function trackEvent(name: string, payload: Record) { if (typeof window === 'undefined') { @@ -172,20 +164,6 @@ export default function FooterCTA() { } } - async function handleCopyBrew() { - try { - await navigator.clipboard.writeText(BREW_COMMAND); - toast('Copied'); - setCopied(true); - - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current); - } - copyTimeoutRef.current = setTimeout(() => setCopied(false), 1200); - } catch { - toast.error('Could not copy. Select the text and copy it by hand.'); - } - } function handleBetaSubmit(event: React.FormEvent) { event.preventDefault(); @@ -209,8 +187,8 @@ export default function FooterCTA() {
-

Download

-

+

Download

+

macOS 14 or later. Apple Silicon and Intel. About 20 MB.

@@ -237,21 +215,10 @@ export default function FooterCTA() { {BREW_COMMAND} - +
-

+

With no connection to hand,{' '} File > Try Sample Database opens a bundled Chinook SQLite. @@ -297,8 +264,8 @@ export default function FooterCTA() {

-

Stay updated

-

+

Stay updated

+

Release notes and database notes. About one email a month, unsubscribe any time.

{subscriberCount !== null && subscriberCount > 0 && ( diff --git a/resources/js/components/landing/objection-row.tsx b/resources/js/components/landing/objection-row.tsx index c58b272..7bff844 100644 --- a/resources/js/components/landing/objection-row.tsx +++ b/resources/js/components/landing/objection-row.tsx @@ -39,7 +39,7 @@ export default function ObjectionRow() { className={`p-6 sm:p-8 ${cellBorders(i, COLS, QUESTIONS.length)} border-rule`} >

{faq.question}

-

{faq.answer}

+

{faq.answer}

); })} diff --git a/resources/js/components/landing/open-source.tsx b/resources/js/components/landing/open-source.tsx index a216d4b..bd100c4 100644 --- a/resources/js/components/landing/open-source.tsx +++ b/resources/js/components/landing/open-source.tsx @@ -48,7 +48,7 @@ export default function OpenSource({ latestRelease }: Props) {
-
+

TablePro is AGPLv3. All 25 databases, the SQL editor, the data grid, the AI assistant, the MCP server, Safe Mode with Touch ID, SSH tunnels, ER diagrams, the server dashboard, diff --git a/resources/js/components/landing/pricing.tsx b/resources/js/components/landing/pricing.tsx index e1019fd..4ab3f4e 100644 --- a/resources/js/components/landing/pricing.tsx +++ b/resources/js/components/landing/pricing.tsx @@ -7,6 +7,7 @@ import { Ledger, LedgerRow } from '@/components/ui/ledger'; import SectionShell from '@/components/ui/section-shell'; import { Availability, CheckGlyph } from '@/components/ui/glyph'; import { PROSE_LINK } from '@/components/ui/prose-link'; +import { PANEL_TITLE } from '@/components/ui/grid-cell'; type BillingCycle = 'monthly' | 'yearly' | 'lifetime'; @@ -153,7 +154,7 @@ function PricingCard({ tier, cycle, discountCode, discount, paymentProvider, tea return (

-

{tier.name}

+

{tier.name}

{tier.description}

@@ -225,7 +226,7 @@ function PricingCard({ tier, cycle, discountCode, discount, paymentProvider, tea
{tier.summary ? ( -

+

{tier.summary}

) : ( diff --git a/resources/js/components/landing/safety.tsx b/resources/js/components/landing/safety.tsx index c3b25c4..e6df1c3 100644 --- a/resources/js/components/landing/safety.tsx +++ b/resources/js/components/landing/safety.tsx @@ -222,7 +222,7 @@ export default function Safety() {

{cell.label}

-

{cell.body}

+

{cell.body}

))}
diff --git a/resources/js/components/landing/sponsors.tsx b/resources/js/components/landing/sponsors.tsx index 9109372..0cdf664 100644 --- a/resources/js/components/landing/sponsors.tsx +++ b/resources/js/components/landing/sponsors.tsx @@ -113,7 +113,7 @@ function SponsorMark({ sponsor }: { sponsor: Sponsor }) { svg]:h-full [&>svg]:w-auto [&>svg]:max-w-full`} + className={`${MARK_CLASS} block opacity-80 grayscale transition-[opacity,filter] duration-(--dur-state) ease-(--ease-feedback) group-hover:opacity-100 group-hover:grayscale-0 [&>svg]:h-full [&>svg]:w-auto [&>svg]:max-w-full group-focus-within:opacity-100 group-focus-within:grayscale-0`} dangerouslySetInnerHTML={{ __html: sponsor.svgContent }} /> ); @@ -127,7 +127,7 @@ function SponsorMark({ sponsor }: { sponsor: Sponsor }) { height={sponsor.height} loading="lazy" decoding="async" - className={`${MARK_CLASS} object-contain opacity-70 grayscale transition-[opacity,filter] duration-(--dur-state) ease-(--ease-feedback) group-hover:opacity-100 group-hover:grayscale-0 group-hover:dark:grayscale dark:invert`} + className={`${MARK_CLASS} object-contain opacity-70 grayscale transition-[opacity,filter] duration-(--dur-state) ease-(--ease-feedback) group-hover:opacity-100 group-hover:grayscale-0 group-hover:dark:grayscale dark:invert group-focus-within:opacity-100 group-focus-within:grayscale-0 group-focus-within:dark:grayscale`} /> ); } @@ -171,7 +171,7 @@ export default function Sponsors() { aria-label="Become a sponsor" className={`${CELL_CLASS} ${borders}`} > - + {cell.sponsor.description && ( - + {cell.sponsor.description} )} diff --git a/resources/js/components/landing/workbench.tsx b/resources/js/components/landing/workbench.tsx index 8551e3b..0e02209 100644 --- a/resources/js/components/landing/workbench.tsx +++ b/resources/js/components/landing/workbench.tsx @@ -200,7 +200,7 @@ export default function Workbench() { {row.index}

{row.title}

-

+

{row.body}

diff --git a/resources/js/components/ui/copy-button.tsx b/resources/js/components/ui/copy-button.tsx new file mode 100644 index 0000000..649a789 --- /dev/null +++ b/resources/js/components/ui/copy-button.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef, useState } from 'react'; +import { Check, Copy } from 'lucide-react'; +import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; + +interface CopyButtonProps { + /** What lands on the clipboard. */ + value: string; + /** Names the thing being copied, for the button's accessible name. */ + label: string; + className?: string; +} + +/** + * Copy to clipboard, with the tick that confirms it. + * + * The identical logic existed twice — the MCP config block and the Homebrew + * command — down to the same 1200ms timeout, the same toast strings and the + * same cleanup effect, but with forked chrome: one had a visible boundary and + * one did not. + * + * The timeout is cleared on unmount and before each restart, because a copy + * button that unmounts mid-timer otherwise sets state on a gone component. + */ +export default function CopyButton({ value, label, className }: CopyButtonProps) { + const [copied, setCopied] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect( + () => () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, + [], + ); + + async function handleCopy() { + try { + await navigator.clipboard.writeText(value); + toast('Copied'); + setCopied(true); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => setCopied(false), 1200); + } catch { + toast.error('Could not copy. Select the text and copy it by hand.'); + } + } + + return ( + + ); +} diff --git a/resources/js/components/ui/database-mark.tsx b/resources/js/components/ui/database-mark.tsx index a4c38e3..14ecf99 100644 --- a/resources/js/components/ui/database-mark.tsx +++ b/resources/js/components/ui/database-mark.tsx @@ -20,7 +20,7 @@ export default function DatabaseMark({ icon, monogram, name }: DatabaseMarkProps height={40} loading="lazy" decoding="async" - className="size-10 object-contain opacity-40 grayscale transition-[opacity,filter] duration-(--dur-state) ease-(--ease-feedback) group-hover:opacity-100 group-hover:grayscale-0 dark:invert group-hover:dark:invert-0" + className="size-10 object-contain opacity-40 grayscale transition-[opacity,filter] duration-(--dur-state) ease-(--ease-feedback) group-hover:opacity-100 group-hover:grayscale-0 dark:invert group-hover:dark:invert-0 group-focus-within:opacity-100 group-focus-within:grayscale-0 group-focus-within:dark:invert-0" /> ); } @@ -29,7 +29,7 @@ export default function DatabaseMark({ icon, monogram, name }: DatabaseMarkProps diff --git a/resources/js/components/ui/faq-list.tsx b/resources/js/components/ui/faq-list.tsx index da6e54c..fcfcb29 100644 --- a/resources/js/components/ui/faq-list.tsx +++ b/resources/js/components/ui/faq-list.tsx @@ -1,5 +1,6 @@ import { cn } from '@/lib/utils'; import type { FaqItem } from '@/data/faqs'; +import { ITEM_TITLE } from '@/components/ui/grid-cell'; function Column({ items, className }: { items: FaqItem[]; className?: string }) { return ( @@ -9,8 +10,8 @@ function Column({ items, className }: { items: FaqItem[]; className?: string }) key={faq.question} className={cn('p-6 sm:p-8', i < items.length - 1 && 'border-b border-rule')} > -

{faq.question}

-

{faq.answer}

+

{faq.question}

+

{faq.answer}

))}
diff --git a/resources/js/components/ui/grid-cell.tsx b/resources/js/components/ui/grid-cell.tsx index fc96d3a..96bfc15 100644 --- a/resources/js/components/ui/grid-cell.tsx +++ b/resources/js/components/ui/grid-cell.tsx @@ -116,6 +116,21 @@ export const CELL_DENSITY = { export type CellDensity = keyof typeof CELL_DENSITY; +/** + * The two heading sizes a cell can carry, named so the choice stops being made + * ad hoc. + * + * `PANEL_TITLE` heads a cell that is the whole point of its cell — the download + * panel, a pricing tier. `ITEM_TITLE` heads one entry in a list of like things — + * a question, a capability, a feature row — where a larger size would make every + * item shout. + * + * Both are h3s. This was three sites at one size and twenty-three at the other, + * with nothing recording which was which. + */ +export const PANEL_TITLE = 'text-lg font-semibold text-foreground'; +export const ITEM_TITLE = 'text-base font-semibold text-foreground'; + interface GridCellProps { className?: string; /** `stripe` fills the cell with the page gutter's 45 degree hatch. */ diff --git a/resources/js/components/ui/ledger.tsx b/resources/js/components/ui/ledger.tsx index fcf0144..6709128 100644 --- a/resources/js/components/ui/ledger.tsx +++ b/resources/js/components/ui/ledger.tsx @@ -46,7 +46,7 @@ export function LedgerRow({ label, children, aside, accent, className }: LedgerR {label} {aside && {aside}} -
{children}
+
{children}
); } diff --git a/resources/js/pages/Blog/Index.tsx b/resources/js/pages/Blog/Index.tsx index 1d4c632..e22e86e 100644 --- a/resources/js/pages/Blog/Index.tsx +++ b/resources/js/pages/Blog/Index.tsx @@ -79,10 +79,10 @@ export default function BlogIndex({ posts, downloadUrls, githubStars }: Props) { {post.readingMinutes} min read
-

+

{post.title}

-

+

{post.description}

{post.tags.length > 0 && ( diff --git a/resources/js/pages/Blog/Post.tsx b/resources/js/pages/Blog/Post.tsx index 01d1816..73b84ab 100644 --- a/resources/js/pages/Blog/Post.tsx +++ b/resources/js/pages/Blog/Post.tsx @@ -164,10 +164,10 @@ export default function BlogPost({ post, relatedPosts, downloadUrls, githubStars {rp.readingMinutes} min read
-

+

{rp.title}

-

+

{rp.description}

diff --git a/resources/js/pages/Compare.tsx b/resources/js/pages/Compare.tsx index 7d514f5..401ec0b 100644 --- a/resources/js/pages/Compare.tsx +++ b/resources/js/pages/Compare.tsx @@ -11,6 +11,8 @@ import { AppleGlyph, Availability, CheckGlyph } from '@/components/ui/glyph'; import FaqList from '@/components/ui/faq-list'; import ThemedImage from '@/components/ui/themed-image'; import DataTable, { TABLE_COLUMN_RULE, TABLE_ROW_RULE } from '@/components/ui/data-table'; +import { ITEM_TITLE } from '@/components/ui/grid-cell'; +import ClosingCta from '@/components/landing/closing-cta'; interface Props { slug: string; @@ -178,7 +180,7 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) { download: 'Download size', }; return ( -
+

{labels[metric]}

{comp.benchmarks!.tablePro[metric]} @@ -220,7 +222,7 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) { {/* Competitor pros */}
-

{comp.name}

+

{comp.name}

    {comp.prosCompetitor.map((pro) => (
  • @@ -250,8 +252,8 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) { {idx + 1}
-

{step.title}

-

{step.description}

+

{step.title}

+

{step.description}

))} @@ -290,11 +292,11 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) {

Choose TablePro if...

-

{comp.chooseTablePro}

+

{comp.chooseTablePro}

-

Choose {comp.name} if...

-

{comp.chooseCompetitor}

+

Choose {comp.name} if...

+

{comp.chooseCompetitor}

@@ -303,7 +305,7 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) {
-

+

{comp.verdict}

@@ -324,32 +326,8 @@ export default function Compare({ slug, downloadUrls, githubStars }: Props) { )} - - {/* CTA */} -
- - - -
-

- Try TablePro for free. -

-

- Free and open-source. No account required. -

-
- -
-
- -
- -
+ +
); } diff --git a/resources/js/pages/DatabaseClient.tsx b/resources/js/pages/DatabaseClient.tsx index 950433b..d1bfa7e 100644 --- a/resources/js/pages/DatabaseClient.tsx +++ b/resources/js/pages/DatabaseClient.tsx @@ -10,6 +10,8 @@ import Button from '@/components/ui/button'; import { AppleGlyph, CheckGlyph } from '@/components/ui/glyph'; import FaqList from '@/components/ui/faq-list'; import ThemedImage from '@/components/ui/themed-image'; +import { ITEM_TITLE } from '@/components/ui/grid-cell'; +import ClosingCta from '@/components/landing/closing-cta'; interface Props { slug: string; @@ -162,8 +164,8 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop i % 2 === 0 ? 'sm:border-r' : '' } ${i < db.features.length - 2 ? 'border-b' : i < db.features.length - 1 ? 'max-sm:border-b' : ''}`} > -

{feature.title}

-

{feature.description}

+

{feature.title}

+

{feature.description}

))}
@@ -245,7 +247,7 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop {/* Code snippet */}
- + {db.snippet}
@@ -253,7 +255,7 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop {/* Connection details */}
-

Supported versions

+

Supported versions

{db.supportedVersions}

Connection types

@@ -293,8 +295,8 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop {idx + 1}
-

{step.name}

-

{step.text}

+

{step.name}

+

{step.text}

))} @@ -318,37 +320,14 @@ export default function DatabaseClient({ slug, downloadUrls, githubStars }: Prop )} - - {/* CTA */} -
- - - -
-

- Try TablePro for free. -

-

- Free and open-source. macOS 14+. Apple Silicon and Intel. -

-
- - -
-
- -
- -
+ +
); } diff --git a/resources/js/pages/Download.tsx b/resources/js/pages/Download.tsx index 1b1f7d9..9ab6015 100644 --- a/resources/js/pages/Download.tsx +++ b/resources/js/pages/Download.tsx @@ -194,7 +194,7 @@ export default function Download({ downloadUrls, githubStars }: Props) { {/* Older versions */}

Older versions

-

+

Need a previous release? All versions are available on GitHub.

-

+

TablePro collects minimal data, never touches your database contents, and lets you opt out of analytics. The codebase is{' '} open source on GitHub under the AGPLv3, so you can verify exactly what is collected and how it is handled.

-

+

This policy covers the desktop and mobile Application, the Website at tablepro.app, and the account portal at tablepro.app/account.

-

+

TablePro is the data controller for personal information described in this policy. For privacy questions, email{' '} hello@tablepro.app.

-

Anonymous usage analytics (desktop app)

-

+

Anonymous usage analytics (desktop app)

+

The Application can send anonymous usage analytics. It is{' '} enabled by default and can be turned off in{' '} Settings > General > "Share anonymous usage data".

-

+

When enabled, a 24-hour heartbeat sends three things to{' '} https://api.tablepro.app/v1/analytics:

-
    +
    • Anonymous machine ID: SHA-256 hash of your hardware UUID. The raw UUID is never sent.
    • Environment: app version, macOS version, CPU architecture (arm64/x86_64), language.
    • Usage: which database engine types you connect to (e.g. "mysql", "postgresql") and the number of open connections.
    -

    +

    That's it. No connection details, no hosts, no credentials, no queries, no database contents. All payloads are signed with HMAC-SHA256.

    Update checks

    -

    +

    The Application uses{' '} Sparkle to check for updates. This sends your{' '} app version, macOS version, and CPU architecture to our update server. Update checks cannot be disabled separately.

    License validation

    -

    +

    If you have entered a paid License Key, the Application sends the License Key to our server when it starts and periodically afterward to confirm it is valid. No other data is sent in that request.

    License purchase and account portal

    -

    +

    When you purchase a License Key, our payment processor (LemonSqueezy or Polar, depending on your region) collects the data needed to complete the transaction: your email address, billing address, and payment information. We receive your email, License Key, and order metadata; we do not see your full payment details.

    -

    +

    The account portal at{' '} tablepro.app/account{' '} uses magic-link authentication. We store your email and an authentication token for that purpose.

    Newsletter and beta signup

    -

    +

    If you subscribe to the newsletter or apply for beta access, we store your email address for that purpose only. You can unsubscribe at any time from any email we send.

    Server logs

    -

    +

    Our servers record standard request logs (IP address, user agent, timestamp, URL) for security and abuse prevention. These logs are retained for up to 90 days.

    -
      +
      • No database contents or queries. SQL you write and data you fetch never leave your machine.
      • No connection credentials. Database hosts, usernames, and passwords stay in the macOS Keychain.
      • No personal information in the desktop app beyond the email used at purchase.
      • @@ -116,7 +117,7 @@ export default function Privacy({ downloadUrls }: Props) { -
          +
          • Anonymous analytics: understand which app versions, OS versions, and database types our users run, to prioritise compatibility and bug fixes.
          • License validation: confirm a License Key is valid and active.
          • Updates: deliver new versions of the Application.
          • @@ -127,8 +128,8 @@ export default function Privacy({ downloadUrls }: Props) { -

            For users in the European Economic Area or the United Kingdom, the legal bases under GDPR / UK GDPR are:

            -
              +

              For users in the European Economic Area or the United Kingdom, the legal bases under GDPR / UK GDPR are:

              +
              • Contract (Art. 6(1)(b)): processing payment, providing the License Key, account portal access.
              • Legitimate interest (Art. 6(1)(f)): anonymous analytics, abuse detection, server logs, retention of business records.
              • Consent (Art. 6(1)(a)): newsletter subscriptions, optional features you enable.
              • @@ -137,26 +138,26 @@ export default function Privacy({ downloadUrls }: Props) { -

                We share personal data only with the providers needed to operate the Services:

                -
                  +

                  We share personal data only with the providers needed to operate the Services:

                  +
                  • LemonSqueezy or Polar: payment processing for License Key purchases.
                  • Email delivery providers: transactional emails (magic links, receipts, newsletter). We use providers that do not sell or share contact data.
                  • Hosting providers: server infrastructure for the Website, account portal, and analytics endpoint.
                  • Plausible Analytics (self-hosted): aggregate, cookie-less Website analytics. No personal identifiers, no IP storage.
                  -

                  +

                  We do not sell, rent, or share personal data with advertisers.

                  -

                  +

                  Our servers operate in multiple regions. When you interact with TablePro, your data may be transferred to or processed in countries outside your own. Where required, transfers from the EEA / UK rely on Standard Contractual Clauses or other approved mechanisms.

                  -
                    +
                    • Anonymous analytics: aggregated indefinitely; the SHA-256 machine ID has no link to your identity.
                    • Account and license data: kept while your license is active and for up to 7 years afterward for tax and audit purposes.
                    • Newsletter subscribers: until you unsubscribe.
                    • @@ -166,10 +167,10 @@ export default function Privacy({ downloadUrls }: Props) { -

                      +

                      Subject to local law, you have the right to:

                      -
                        +
                        • Access: request a copy of personal data we hold about you.
                        • Rectification: correct inaccurate data.
                        • Erasure: ask us to delete your data, subject to legal retention obligations.
                        • @@ -179,44 +180,44 @@ export default function Privacy({ downloadUrls }: Props) {
                        • Withdraw consent: at any time, where processing is based on consent.
                        • Lodge a complaint: with your local data protection authority.
                        -

                        +

                        To exercise any of these rights, email{' '} hello@tablepro.app. We respond within 30 days.

                        -

                        +

                        If you are a California resident, the California Consumer Privacy Act gives you the right to know what personal information we collect, to request deletion, to opt out of "sale" of personal information, and to non-discrimination for exercising these rights. We do not sell personal information.

                        -

                        +

                        TablePro is not directed at children under 16. We do not knowingly collect personal data from children. If you believe a child has provided us data, contact us and we will delete it.

                        -

                        +

                        We use industry-standard measures to protect data: HTTPS for all network traffic, password hashing with modern algorithms, scoped API tokens, and least-privilege access for our team. No system is perfectly secure; if you suspect a vulnerability, email{' '} hello@tablepro.app.

                        -

                        +

                        The marketing site uses two functional cookies. No tracking, no advertising, no third-party SDKs.

                        -
                          +
                          • nl_dismissed_at (90 days): records when you dismissed the newsletter prompt so we don't reshow it. Lawful basis: legitimate interest.
                          • nl_subscribed (365 days): records that you subscribed so we don't reprompt. Lawful basis: legitimate interest, performance of a subscription you initiated.
                          -

                          Sensitive data stays on your Mac:

                          -
                            +

                            Sensitive data stays on your Mac:

                            +
                            • Database credentials: macOS Keychain.
                            • Query history: local SQLite database.
                            • App settings: standard macOS UserDefaults.
                            • @@ -225,7 +226,7 @@ export default function Privacy({ downloadUrls }: Props) { -

                              +

                              The Application source is on{' '} GitHub under the AGPLv3. The analytics code is in{' '} TablePro/Core/Services/AnalyticsService.swift. You can verify what is sent. @@ -233,13 +234,13 @@ export default function Privacy({ downloadUrls }: Props) { -

                              +

                              We may update this policy. Material changes will be posted on this page with a new "Last updated" date and, where required by law, notified to users.

                              -

                              +

                              For privacy questions, security reports, or anything else, email{' '} hello@tablepro.app.

                              diff --git a/resources/js/pages/RefundPolicy.tsx b/resources/js/pages/RefundPolicy.tsx index 973a58f..36ede51 100644 --- a/resources/js/pages/RefundPolicy.tsx +++ b/resources/js/pages/RefundPolicy.tsx @@ -32,7 +32,7 @@ export default function RefundPolicy({ downloadUrls }: Props) { -
                              +

                              7-day money-back guarantee on all paid plans. The window starts on the date of purchase, or on the most recent renewal for yearly subscriptions.

                              diff --git a/resources/js/pages/Terms.tsx b/resources/js/pages/Terms.tsx index 716a491..a2253ba 100644 --- a/resources/js/pages/Terms.tsx +++ b/resources/js/pages/Terms.tsx @@ -36,7 +36,7 @@ export default function Terms({ downloadUrls }: Props) {
                              -

                              +

                              These Terms of Service govern your use of TablePro, the website at{' '} tablepro.app, the desktop application, the iOS application, and any related services. By downloading, installing, accessing, or using @@ -45,7 +45,7 @@ export default function Terms({ downloadUrls }: Props) {

                              -
                                +
                                • "TablePro", "we", "us", "our" refers to the TablePro project and its maintainers.
                                • "Application" means the TablePro desktop and mobile software, including all updates and plugins.
                                • "Website" means tablepro.app, docs.tablepro.app, and any related domains we operate.
                                • @@ -56,39 +56,39 @@ export default function Terms({ downloadUrls }: Props) { -

                                  +

                                  The Application source code is licensed under the{' '} GNU Affero General Public License v3 (AGPLv3). You may use, modify, and distribute it under those terms. The full license text is at{' '} github.com/TableProApp/TablePro/LICENSE.

                                  -

                                  +

                                  The free tier covers core functionality and is enough for most personal and commercial use. No registration is required to use the free tier.

                                  -

                                  +

                                  Some advanced features require a paid License Key. When you purchase a License Key, you receive a non-exclusive, non-transferable right to enable those features for the number of devices and the duration covered by your plan.

                                  -

                                  +

                                  License Keys are personal. You may not share, sublicense, sell, rent, or otherwise transfer a License Key to any third party. Sharing a License Key may result in immediate termination without refund.

                                  -

                                  +

                                  Refund eligibility is described in the{' '} Refund Policy.

                                  -

                                  +

                                  License Keys are managed through an account portal accessed by magic link sent to the email address you used at purchase. You are responsible for keeping that email account secure. We are not responsible for unauthorized access to your account caused by a compromised email account.

                                  -

                                  You agree not to:

                                  -
                                    +

                                    You agree not to:

                                    +
                                    • Use the Application or Services in violation of any law or third-party right.
                                    • Reverse engineer or circumvent license enforcement on paid features beyond what AGPLv3 explicitly permits.
                                    • Attempt to gain unauthorized access to our infrastructure or other users' accounts.
                                    • @@ -98,100 +98,100 @@ export default function Terms({ downloadUrls }: Props) { -

                                      You are solely responsible for:

                                      -
                                        +

                                        You are solely responsible for:

                                        +
                                        • Securing your database credentials and License Keys.
                                        • Backing up your data before running queries through the Application.
                                        • Having authorization to access the databases you connect to.
                                        • Following applicable laws when using the Application and Services.
                                        -

                                        +

                                        You acknowledge and agree that your use of the Application can affect your databases, and you accept{' '} sole responsibility for any errors, malfunctions, or corruption of any database caused by your use of the Application, including queries you run, scripts you execute, and changes you commit.

                                        -

                                        +

                                        THE APPLICATION AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ACCURACY, OR UNINTERRUPTED OPERATION.

                                        -

                                        +

                                        We do not warrant that the Application will meet your requirements, be compatible with any specific database version, operate without error, or that any errors will be corrected.

                                        -

                                        +

                                        TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL TABLEPRO OR ITS CONTRIBUTORS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO: LOSS OF DATA, LOSS OF DATABASE CONTENTS, DATA CORRUPTION, LOSS OF PROFITS, LOSS OF BUSINESS, BUSINESS INTERRUPTION, LOSS OF GOODWILL, OR THE COST OF SUBSTITUTE GOODS OR SERVICES, ARISING OUT OF OR RELATED TO YOUR USE OF THE APPLICATION OR SERVICES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

                                        -

                                        +

                                        Where liability cannot be disclaimed under applicable law, our total cumulative liability for all claims arising from or related to the Application and Services is limited to the greater of (a) ten US dollars ($10) or (b) the total amount you paid us during the twelve (12) months preceding the event giving rise to the claim.

                                        -

                                        +

                                        Some jurisdictions do not allow the exclusion or limitation of certain damages. In such jurisdictions, our liability is limited to the minimum extent permitted by law.

                                        -

                                        +

                                        You agree to indemnify and hold harmless TablePro and its contributors from any claim, demand, loss, or damage (including reasonable legal fees) arising out of your use of the Application or Services in violation of these Terms or any third-party right.

                                        -

                                        +

                                        We may release updates, fixes, and new features at our discretion. We have no obligation to maintain, update, or provide support for any version of the Application. Older versions may stop receiving security updates.

                                        -

                                        +

                                        Some Services (license validation, update checks) require network access to function. We do not guarantee continuous availability of these Services and may discontinue them with reasonable notice.

                                        -

                                        +

                                        You may stop using the Application and Services at any time. We may suspend or terminate your access to paid features without refund if you breach these Terms, including the Acceptable Use clause.

                                        -

                                        +

                                        Sections that by their nature survive termination (Disclaimer, Limitation of Liability, Indemnification, Governing Law) will continue to apply after termination.

                                        -

                                        +

                                        Our handling of personal data is described in the{' '} Privacy Policy. By using the Application or Services, you agree to that policy.

                                        -

                                        +

                                        We may update these Terms from time to time. Material changes will be announced on this page with a new "Last updated" date. Continuing to use the Application or Services after the update means you accept the new Terms.

                                        -

                                        +

                                        These Terms are governed by the laws of the Socialist Republic of Vietnam, without regard to conflict of law principles. Any dispute arising out of or relating to these Terms or your use of the Application or Services shall be resolved in the competent courts of Vietnam.

                                        -

                                        +

                                        If you reside in a jurisdiction with mandatory consumer protection laws, those laws apply to the extent they cannot be waived.

                                        -

                                        +

                                        If any provision of these Terms is held invalid or unenforceable by a court of competent jurisdiction, the remaining provisions remain in full force and effect.

                                        -

                                        +

                                        These Terms, together with the AGPLv3 license, the Privacy Policy, and the Refund Policy, constitute the entire agreement between you and TablePro regarding the Application and Services and supersede any prior agreements.

                                        -

                                        +

                                        For questions about these Terms, email{' '} hello@tablepro.app{' '} or open an issue at{' '}