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
4 changes: 3 additions & 1 deletion app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Services\Releases\PlatformCatalog;
use App\Support\Assets\AssetManifest;
use App\Support\Content\ContentRepository;
use App\Support\Content\Testimonials;
use App\Support\Pricing\Checkout;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
Expand Down Expand Up @@ -40,7 +41,7 @@ class HomeController extends Controller
*/
private array $decoded = [];

public function __invoke(ContentRepository $content, PlatformCatalog $platforms, AssetManifest $assets, Checkout $checkout): Response
public function __invoke(ContentRepository $content, PlatformCatalog $platforms, AssetManifest $assets, Checkout $checkout, Testimonials $testimonials): Response
{
$locale = App::getLocale();

Expand All @@ -49,6 +50,7 @@ public function __invoke(ContentRepository $content, PlatformCatalog $platforms,
'engines' => $this->engines(),
'categories' => $this->categories($content->page('databases/index', $locale)),
'iosEngines' => $this->iosEngines($platforms),
'testimonials' => $testimonials->forLocale($locale),
'checkout' => $checkout->props(),
'lcpAsset' => $assets->lcpDescriptor('mac-hero-window', $locale),
]);
Expand Down
81 changes: 81 additions & 0 deletions app/Support/Content/Testimonials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace App\Support\Content;

use Illuminate\Support\Facades\File;

/**
* @phpstan-type Quote array{id: string, name: string, handle: string|null, platform: string, url: string, date: string, lang: string, text: string, translations: array<string, string>}
* @phpstan-type ShownQuote array{id: string, name: string, handle: string|null, platform: string, url: string, avatar: string, lang: string, text: string, translation: string|null, translatedFrom: string|null}
*/
final class Testimonials
{
public const int SHOWN = 6;

/**
* @var list<Quote>|null
*/
private ?array $quotes = null;

/**
* The quotes a page in this locale shows: those written in its language first, then the rest in data order.
*
* @return list<ShownQuote>
*/
public function forLocale(string $locale): array
{
$quotes = $this->quotes();
$own = array_filter($quotes, fn(array $quote): bool => self::sameLanguage($quote['lang'], $locale));
$other = array_filter($quotes, fn(array $quote): bool => ! self::sameLanguage($quote['lang'], $locale));
$shown = array_slice([...$own, ...$other], 0, self::SHOWN);

return array_map(fn(array $quote): array => $this->present($quote, $locale), $shown);
}

// zh-Hant readers read a zh-Hans quote as written; the lang attribute picks the glyphs.
public static function sameLanguage(string $quoteLang, string $locale): bool
{
return self::language($quoteLang) === self::language($locale);
}

private static function language(string $tag): string
{
return strtolower(explode('-', $tag)[0]);
}

/**
* @param Quote $quote
* @return ShownQuote
*/
private function present(array $quote, string $locale): array
{
$translated = ! self::sameLanguage($quote['lang'], $locale);

return [
'id' => $quote['id'],
'name' => $quote['name'],
'handle' => $quote['handle'],
'platform' => $quote['platform'],
'url' => $quote['url'],
'avatar' => "/images/testimonials/{$quote['id']}.webp",
'lang' => $quote['lang'],
'text' => $quote['text'],
'translation' => $translated ? ($quote['translations'][$locale] ?? null) : null,
'translatedFrom' => $translated ? self::language($quote['lang']) : null,
];
}

/**
* @return list<Quote>
*/
private function quotes(): array
{
if ($this->quotes === null) {
/** @var array{quotes: list<Quote>} $data */
$data = File::json(resource_path('data/testimonials.json'), JSON_THROW_ON_ERROR);
$this->quotes = $data['quotes'];
}

return $this->quotes;
}
}
13 changes: 10 additions & 3 deletions docs/rebuild/design/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -1357,7 +1357,7 @@ The ceiling drops from 320ms to 240ms, and `--dur-row` goes with the row bar.

### 8.1 Home (`/`, `/vi`)

The ten sections, their ids, aliases and slots are sitemap §D's. `HomepageRenderTest` and sitemap §G.3 pin these ids.
The eleven sections, their ids, aliases and slots are sitemap §D's. `HomepageRenderTest` and sitemap §G.3 pin these ids.

```
SiteHeader
Expand Down Expand Up @@ -1397,10 +1397,17 @@ SiteHeader
§8 COMING FROM ANOTHER APP #switch (alias #compare)
text at text width: importers named (passwords where true) · Open Project Folder
Compare TablePro with other clients → (/compare)
§9 PRICING #pricing (alias #license)
§9 WHAT DEVELOPERS SAY #testimonials cells, flush: the quotes close on the next join
H2 + lead ("quoted from public posts, each linked to the original")
six quotes as cells, 1 / 2 / 3 across (768, 1280), the reader's language first. Each cell: the quote at body 500,
its marks in the quoted language with the opening one hanging in the padding from 768; a quote in another language
shows its translation, with the original behind a Disclosure labelled "Translated from …"; on the cell's floor the
name (small 500), handle · platform ↗ (muted) and the author's own photo, 40px, 6px radius, a 10% hairline, on the right.
No dates, ratings, platform logos or Review markup. Photos are 96px WebP under public/images/testimonials
§10 PRICING #pricing (alias #license)
H2 + lead (free core · optional paid features on Mac · open source) · BillingCycleControl + caption
PricingCard ×3 as joined cells (compact: no Includes list) · Compare plans → (/pricing) · Prices in USD … line
§10 OPEN SOURCE AND GET STARTED #open-source background, not surface
§11 OPEN SOURCE AND GET STARTED #open-source background, not surface
AGPLv3 · View on GitHub ↗ · one sentence on how the project is funded
PlatformActions as cells, flush on the footer's rule: [Download for Mac] primary + AvailabilityLine · App Store badge + AvailabilityLine
SiteFooter
Expand Down
7 changes: 4 additions & 3 deletions docs/rebuild/design/sitemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Column notes:

| Path EN | Path VI | Family | Intent / search need | H1 (EN) | Sections, ordered by reader question | Data | Slots | Key internal links | Indexable |
|---|---|---|---|---|---|---|---|---|---|
| `/` | `/vi` | Home | Brand and navigational searches; a first evaluation ("what is it, does it fit me, where do I get it") | "A native database client for developers." (final wording comes from the positioning decision) | See §D: 10 sections, Sponsors third | platforms, engines, pricing, sponsors, facts; GitHub latest release (cached, dated fallback) | mac-hero-window, mac-hero-window-mobile, mac-query-autocomplete, mac-edit-preview-sql, mac-compare-sync-structure, mac-data-files-window, mac-connection-ssh-form, mac-safe-mode-touchid, mac-ai-chat, ios-connection-list | /features and every feature page, /databases, /download, /ios, /pricing, /compare, /account | yes (pair) |
| `/` | `/vi` | Home | Brand and navigational searches; a first evaluation ("what is it, does it fit me, where do I get it") | "A native database client for developers." (final wording comes from the positioning decision) | See §D: 11 sections, Sponsors third | platforms, engines, pricing, sponsors, facts; GitHub latest release (cached, dated fallback) | mac-hero-window, mac-hero-window-mobile, mac-query-autocomplete, mac-edit-preview-sql, mac-compare-sync-structure, mac-data-files-window, mac-connection-ssh-form, mac-safe-mode-touchid, mac-ai-chat, ios-connection-list | /features and every feature page, /databases, /download, /ios, /pricing, /compare, /account | yes (pair) |
| `/download` | `/vi/download` | Platform | "download TablePro", "TablePro mac", `brew install tablepro`. Linked from the docs navbar, the purchase email and the org README | "Download TablePro" | 1) Current release: version, date, release notes ↗ 2) **Mac** (`#mac`): Apple Silicon and Intel buttons, both rendered on the server; DMG sizes from the release API; "Requires macOS 13 Ventura or later"; "Which Mac do I have?"; copyable `brew install --cask tablepro` with a note that Homebrew can trail a release; GitHub Releases 3) **iPhone & iPad** (`#ios`): App Store badge, iOS/iPadOS 18 or later, free with no in-app purchases, link to /ios 4) **Install and first run** (`#install`): drag to Applications; notarized; drivers download the first time you pick an engine (needs a network); sample database. The post-click panel appears only after the click 5) **Updates** (`#updates`): daily check, Settings toggle 6) **Other platforms** (`#other-platforms`): Windows and Linux are not available, with no date 7) **Older versions and release notes** (`#older-versions`) | platforms; GitHub releases API (`releases/latest`, cached, literal fallback) | none (none of the reference download pages shows a screenshot) | /ios, /features, /pricing, docs `/installation` ↗, docs `/changelog` ↗ | yes (pair) |
| `/ios` | `/vi/ios` | Platform | "TablePro iPhone/iPad", "database client iPhone", "PostgreSQL/MySQL app iOS". Linked from the App Store listing (seller URL `/`) | "TablePro for iPhone and iPad" | 1) Hero: free, no in-app purchases, iOS/iPadOS 18 or later, App Store badge, "1.0" from data 2) Databases (`#databases`): the 10 names; Redshift opens when synced; other engines sync in but do not open 3) Browse and edit (`#browse`): cards, filters, FK preview, row editor (needs a primary key), truncate and drop 4) Query (`#query`): highlighted editor, Run/Stop, 200-entry history per device, copy or share JSON/CSV/INSERT, Live Activity with Hide Query 5) Connect securely (`#security`): SSH password or key, host keys, SSL per engine (SQL Server without verification), Keychain, Entra on synced SQL Server 6) Safe Mode and App Lock (`#safe-mode`): 3 levels; Face ID, Touch ID or passcode 7) iPad (`#ipad`): universal app, multiple windows, keyboard shortcuts 8) With your Mac (`#mac`): iCloud sync of connections, groups and tags (off by default; the Mac side needs Starter); `.tablepro` export with optional encrypted passwords (free here); Handoff 9) Shortcuts, widgets, Spotlight (`#automation`) 10) Not on iPhone or iPad (`#limits`) 11) Privacy (`#privacy`): Share Usage Data is off by default 12) Get it, plus a short FAQ | platforms (`ios.*`), engines (`ios` fields), facts (Safe Mode levels, export formats) | ios-connection-list, ipad-table-browse, ios-table-browse, ios-row-edit, ios-query, ios-live-activity, ios-connection-form, ios-safe-mode-confirm, ipad-two-windows, diagram-icloud-sync, mac-handoff-ios, ios-widgets, ios-shortcuts-add-rows, ios-settings-privacy | /download, /features/sync-and-teams, /databases, /privacy#ios-app, docs `/ios` ↗ (only sections that are true for 1.0) | yes (pair) |
| `/pricing` | `/vi/pricing` | Commerce | "TablePro price", "license", "team"; also visitors who arrive from in-app upgrade links through `/#pricing` | "Pricing" | 1) One line from data: the Mac app is free, a license unlocks the paid features, and the iPhone and iPad app is free 2) **Plans** (`#plans`): Free, Starter and Team cards; monthly/yearly/lifetime toggle; unit, minimum and total in words; seat stepper 5-200; Buy starts `POST /checkout` 3) **What a license unlocks** (`#features`): the paid features from `pricing.json`, each with its tier, one line and a link to its feature page, plus an iPhone column that reads "free, nothing gated" 4) **How licenses work** (`#license`): Starter covers 2 Macs; a Team seat is one activated Mac; revalidated every 7 days; 30-day offline grace; when a subscription ends the paid features lock and everything else keeps working; Lifetime is paid once with no expiry date 5) **Billing** (`#billing`): Polar is the merchant of record; prices in USD; Polar handles sales tax or VAT and receipts; "Billing & invoices" in Account for Polar purchases 6) **Refunds** (`#refunds`): 7 days on every plan, link to /refund-policy 7) **Team** (`#team`): seats, invites, Priority support (Team emails answered first, within one business day) 8) **Open source** (`#open-source`): AGPLv3; source builds gate the same features 9) **FAQ** (`#faq`): the licensing subset of the FAQ data | pricing; FAQ data (licensing subset) | none | each paid feature's page anchor, /refund-policy, /account?locale=, /download, /faq#licensing, /terms | yes (pair) |
Expand Down Expand Up @@ -502,8 +502,9 @@ The same structure serves `/` and `/vi`. The ids are locale-neutral.
| 6 | `ai` (`mcp`) | **AI and MCP** | Does it work with AI tools, and what can the AI touch? | Bring your own provider (named); Agent mode; a local MCP server that is off by default; the permission layers in one line; free → /features/ai-mcp | mac-ai-chat | facts (providers) |
| 7 | `platforms` (`mobile`) | **Mac, iPhone and iPad** | Is there a phone app, and how does it relate to the Mac app? | Two cards, described positively and with no parity claim. **Mac**: the full workbench, requirement, Download. **iPhone and iPad**: browse, edit, query and SSH on the go; free; App Store. One line on iCloud Sync and Handoff (Starter on the Mac) → /ios, /features/sync-and-teams | ios-connection-list | platforms; `download_click{location:'home-platforms', platform}` |
| 8 | `switch` (`compare`) | **Coming from another app** | I already use another client. Can I bring my connections? | Importers named, with passwords where true; Open Project Folder; "Compare TablePro with …" → /compare | none | facts (import sources) |
| 9 | `pricing` (`license`) | **Pricing** | What is free, and what costs money? | A working compact block: the Free, Starter and Team cards with the cycle toggle and Buy buttons (same component and data as /pricing); one sentence on the free core and open source; "Compare plans →" /pricing. This is the target of `/?ref=…#pricing` from the Mac app | none | pricing; `checkout_started{tier,cycle}`; `download_click{location:'pricing-free'}` |
| 10 | `open-source` | **Open source and get started** | Who builds it, can I read the code, and how do I start? | AGPLv3 and a GitHub link; one sentence on how the project is funded (no superlatives); the final "Download for Mac" plus App Store badge with their availability lines | none | platforms; `download_click{location:'footer-cta', platform}` |
| 9 | `testimonials` | **What developers say** (added 2026-10-10, owner's decision) | Do other developers actually use it? | Six public posts, quoted as written, each with the author's name, handle, own photo and a link to the post; the reader's language first; a quote in another language shows a labelled translation with the original one click away. No dates, ratings or `Review` markup | none (author photos are identity, like sponsor logos) | testimonials.json (sources, dates, translations) |
| 10 | `pricing` (`license`) | **Pricing** | What is free, and what costs money? | A working compact block: the Free, Starter and Team cards with the cycle toggle and Buy buttons (same component and data as /pricing); one sentence on the free core and open source; "Compare plans →" /pricing. This is the target of `/?ref=…#pricing` from the Mac app | none | pricing; `checkout_started{tier,cycle}`; `download_click{location:'pricing-free'}` |
| 11 | `open-source` | **Open source and get started** | Who builds it, can I read the code, and how do I start? | AGPLv3 and a GitHub link; one sentence on how the project is funded (no superlatives); the final "Download for Mac" plus App Store badge with their availability lines | none | platforms; `download_click{location:'footer-cta', platform}` |

**Not on the homepage:**

Expand Down
2 changes: 1 addition & 1 deletion docs/shared-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ string arrives as a prop, so each app passes its own words in its own language.
| --- | --- | --- |
| `resources/css/tokens.css` | `aeba9a5b0dbcadc2285968462c8973217be6a03f349aa8d317f6698ccc5d6a40` | Colour tokens for light and dark, `color-scheme`, the type roles and their `--lh-*` line heights per language, radii, motion, the default border colour, the focus ring, forced colours, the ThemeControl icon and segment states, reduced motion (design-system §2–§4, §7) |
| `resources/css/fonts.css` | `1ca1fc06d1978f008deb7c1d3e0b6421d11df39732612898e8776941228be6c0` | Inter (optical size) and IBM Plex Mono 400, declared latin-ext, vietnamese, latin (architecture §1.11) |
| `resources/css/international.css` | `7bb9d377124bcafb6b8e2c1a0dbd2e9114e6a87ea2bb38ab013a4204ce5cd3e7` | Native Japanese, Korean, simplified and traditional Chinese font stacks in both apps |
| `resources/css/international.css` | `dce122464933097269a0d628baf50582e469b2c9dfacd3a7f64074f1a759bacb` | Native Japanese, Korean, simplified and traditional Chinese font stacks in both apps |
| `resources/views/partials/head-theme.blade.php` | `10d0c109dc1b41f732519b94fa2dfa302fab590dc80089f419b48d36a3710110` | The pre-paint theme script, verbatim from design-system §2.8: light by default, `theme` in `localStorage`, `data-theme-choice`, one `theme-color` |
| `tests/Support/vi-forbidden-variants.php` | `fcf40949323de01bed0be38a53d89256184e6c823acedfd0e0a91513690caa69` | The glossary's forbidden Vietnamese variants (sitemap §E.10), read by `Localization/ContentParityTest` here and `Localization/LangParityTest` in the account app |
| `resources/js/lib/theme.ts` | `2c8b2af95cb12329a87a11743281af2506917ccbd57cc109629b51e4505d9765` | Reading, applying and syncing the theme choice across tabs and both apps (`theme` key; `tablepro:theme-change` event) |
Expand Down
Binary file added public/images/testimonials/acerola-native.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/marvelph-license.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/narr-review.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/pranav-duckdb.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/rockpilp-dbeaver.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/weswinder-x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/testimonials/xlongye-native.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,46 @@ html {
word-break: keep-all;
}

/*
* Testimonials. The marks follow the language of the quoted text, and from
* 768px the opening mark hangs in the cell's padding so the text keeps its edge.
*/
.testimonial-quote {
quotes: "\201C" "\201D" "\2018" "\2019";
}

.testimonial-quote:lang(de) {
quotes: "\201E" "\201C" "\201A" "\2018";
}

.testimonial-quote:lang(fr) {
quotes: "\00AB\00A0" "\00A0\00BB" "\201C" "\201D";
}

.testimonial-quote:lang(ja),
.testimonial-quote:lang(zh-Hant) {
quotes: "\300C" "\300D" "\300E" "\300F";
}

.testimonial-quote > p:first-child::before {
content: open-quote;
}

.testimonial-quote > p:last-child::after {
content: close-quote;
}

@media (min-width: 48rem) {
.testimonial-quote > p:first-child {
position: relative;
}

.testimonial-quote > p:first-child::before {
position: absolute;
right: 100%;
}
}

:lang(ja) {
line-break: strict;
}
Expand Down
15 changes: 11 additions & 4 deletions resources/css/international.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
*/
/* Native script fonts supplement Inter without downloading a second UI font. */
/* Windows draws Yu Gothic too light at 400, so Meiryo comes first where it is installed. */
html:lang(ja) {
:lang(ja) {
--font-sans: "Inter Variable", "Hiragino Kaku Gothic ProN", "Meiryo", "Yu Gothic", "Noto Sans CJK JP", sans-serif;
}
html:lang(ko) {
:lang(ko) {
--font-sans: "Inter Variable", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR", sans-serif;
}
html:lang(zh-Hans) {
:lang(zh-Hans) {
--font-sans: "Inter Variable", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}
html:lang(zh-Hant) {
:lang(zh-Hant) {
--font-sans: "Inter Variable", "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", sans-serif;
}

/* Text marked as another language than the page takes that language's faces: a Japanese quote on an English page. */
@layer base {
:where(:not(html)[lang]) {
font-family: var(--font-sans, var(--default-font-family));
}
}
Loading