website: clean up liveline page, fix dark mode, add OG tags

- Remove 5 ChartPlaceholder stubs that were never replaced with real content
- Add OG meta tags (title, description, og:image 1280x800, twitter:card
  summary_large_image) for rich previews on X, Slack, Discord
- Rename PropsTable to ComparisonTable with configurable headers prop,
  fixing the hardcoded 'Prop/Type/Default' headers on comparison tables
- Remove stale Antigravity/Jetski comparison section
- Switch dark mode from .dark class toggle to prefers-color-scheme media
  query via Tailwind @custom-variant. Remove inline JS script from root.tsx,
  update all .dark selectors in liveline.css, liveline-prism.css to use
  @variant dark from globals.css so strategy is defined in one place
- Fix scrollbar colors: light gray on light backgrounds, subtle white on
  dark backgrounds (was using --color-muted which was invisible in both)
- Update remote access section with traforo link, VPN/firewall context,
  MCP env var docs, and more use cases from docs/remote-access.md
This commit is contained in:
Tommy D. Rossi
2026-02-20 15:44:16 +01:00
parent 11b615c297
commit 3917c5137d
6 changed files with 151 additions and 128 deletions
+9 -7
View File
@@ -478,11 +478,13 @@ export function ChartPlaceholder({ height = 200, label }: { height?: number; lab
Comparison table Comparison table
========================================================================= */ ========================================================================= */
export function PropsTable({ export function ComparisonTable({
title, title,
headers,
rows, rows,
}: { }: {
title?: string; title?: string;
headers: [string, string, string];
rows: Array<[string, string, string]>; rows: Array<[string, string, string]>;
}) { }) {
return ( return (
@@ -511,7 +513,7 @@ export function PropsTable({
> >
<thead> <thead>
<tr> <tr>
{["Prop", "Type", "Default"].map((header) => { {headers.map((header) => {
return ( return (
<th <th
key={header} key={header}
@@ -532,9 +534,9 @@ export function PropsTable({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.map(([prop, type, def]) => { {rows.map(([feature, them, us]) => {
return ( return (
<tr key={prop}> <tr key={feature}>
<td <td
style={{ style={{
padding: "4.8px 12px 4.8px 0", padding: "4.8px 12px 4.8px 0",
@@ -546,7 +548,7 @@ export function PropsTable({
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
{prop} {feature}
</td> </td>
<td <td
style={{ style={{
@@ -559,7 +561,7 @@ export function PropsTable({
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
{type} {them}
</td> </td>
<td <td
style={{ style={{
@@ -571,7 +573,7 @@ export function PropsTable({
borderBottom: "1px solid var(--ll-border)", borderBottom: "1px solid var(--ll-border)",
}} }}
> >
{def} {us}
</td> </td>
</tr> </tr>
); );
+1 -10
View File
@@ -11,19 +11,10 @@ import {
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<html className="" lang="en" suppressHydrationWarning> <html lang="en">
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{/*
Apply .dark class based on OS prefers-color-scheme.
Inline in <head> to prevent FOUC. Listener handles live OS changes.
*/}
<script
dangerouslySetInnerHTML={{
__html: `(function(){var d=document.documentElement.classList,m=matchMedia('(prefers-color-scheme:dark)');function a(){d.toggle('dark',m.matches)}a();m.addEventListener('change',a)})()`,
}}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link <link
rel="preconnect" rel="preconnect"
+42 -27
View File
@@ -14,14 +14,35 @@ import {
Code, Code,
Caption, Caption,
CodeBlock, CodeBlock,
ChartPlaceholder,
Section, Section,
PropsTable, ComparisonTable,
List, List,
OL, OL,
Li, Li,
} from "website/src/components/markdown"; } from "website/src/components/markdown";
export function meta() {
const title = "Playwriter - Control your Chrome with Playwright API";
const description =
"Chrome extension + CLI for browser automation. Full Playwright API on your existing browser. No new windows, no flags, no context bloat.";
const image = "https://playwriter.dev/screenshot@2x.png";
return [
{ title },
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: image },
{ property: "og:image:width", content: "1280" },
{ property: "og:image:height", content: "800" },
{ property: "og:type", content: "website" },
{ property: "og:url", content: "https://playwriter.dev/liveline" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
{ name: "twitter:image", content: image },
];
}
const tocItems = [ const tocItems = [
{ label: "Getting started", href: "#getting-started" }, { label: "Getting started", href: "#getting-started" },
{ label: "How it works", href: "#how-it-works" }, { label: "How it works", href: "#how-it-works" },
@@ -101,7 +122,6 @@ export default function LivelinePage() {
playwriter -s 1 -e "await page.locator('aria-ref=e5').click()" playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
`}</CodeBlock> `}</CodeBlock>
<ChartPlaceholder height={200} label="getting started" />
<Caption> <Caption>
Extension icon green = connected. Gray = not attached to this tab. Extension icon green = connected. Gray = not attached to this tab.
</Caption> </Caption>
@@ -186,7 +206,6 @@ export default function LivelinePage() {
screenshots when spatial layout matters &mdash; grids, dashboards, maps. screenshots when spatial layout matters &mdash; grids, dashboards, maps.
</P> </P>
<ChartPlaceholder height={200} label="snapshot" />
<Caption> <Caption>
Accessibility tree as text. 5&ndash;20KB vs 100KB+ for screenshots. Accessibility tree as text. 5&ndash;20KB vs 100KB+ for screenshots.
</Caption> </Caption>
@@ -217,7 +236,6 @@ export default function LivelinePage() {
and visual modes freely. and visual modes freely.
</P> </P>
<ChartPlaceholder height={260} label="visual labels" />
<Caption> <Caption>
Vimium-style labels. Screenshot + snapshot in one call. Vimium-style labels. Screenshot + snapshot in one call.
</Caption> </Caption>
@@ -286,7 +304,6 @@ export default function LivelinePage() {
<Code>grep</Code> across all loaded scripts. <Code>grep</Code> across all loaded scripts.
</P> </P>
<ChartPlaceholder height={200} label="debugger" />
<Caption> <Caption>
Breakpoints, stepping, variable inspection &mdash; from the CLI. Breakpoints, stepping, variable inspection &mdash; from the CLI.
</Caption> </Caption>
@@ -351,7 +368,6 @@ export default function LivelinePage() {
cancel without saving with <Code>cancelRecording</Code>. cancel without saving with <Code>cancelRecording</Code>.
</P> </P>
<ChartPlaceholder height={200} label="recording" />
<Caption> <Caption>
Native tab capture. 30&ndash;60fps. Survives navigation. Native tab capture. 30&ndash;60fps. Survives navigation.
</Caption> </Caption>
@@ -364,8 +380,9 @@ export default function LivelinePage() {
How Playwriter compares to other browser automation approaches. How Playwriter compares to other browser automation approaches.
</P> </P>
<PropsTable <ComparisonTable
title="vs Playwright MCP" title="vs Playwright MCP"
headers={["", "Playwright MCP", "Playwriter"]}
rows={[ rows={[
["Browser", "Spawns new Chrome", "Uses your Chrome"], ["Browser", "Spawns new Chrome", "Uses your Chrome"],
["Extensions", "None", "Your existing ones"], ["Extensions", "None", "Your existing ones"],
@@ -375,8 +392,9 @@ export default function LivelinePage() {
]} ]}
/> />
<PropsTable <ComparisonTable
title="vs BrowserMCP" title="vs BrowserMCP"
headers={["", "BrowserMCP", "Playwriter"]}
rows={[ rows={[
["Tools", "12+ dedicated tools", "1 execute tool"], ["Tools", "12+ dedicated tools", "1 execute tool"],
["API", "Limited actions", "Full Playwright"], ["API", "Limited actions", "Full Playwright"],
@@ -385,17 +403,9 @@ export default function LivelinePage() {
]} ]}
/> />
<PropsTable <ComparisonTable
title="vs Antigravity (Jetski)"
rows={[
["Tools", "17+ tools", "1 tool"],
["Subagent", "Spawns for each task", "Direct execution"],
["Latency", "High (agent overhead)", "Low"],
]}
/>
<PropsTable
title="vs Claude Browser Extension" title="vs Claude Browser Extension"
headers={["", "Claude Extension", "Playwriter"]}
rows={[ rows={[
["Agent support", "Claude only", "Any MCP client"], ["Agent support", "Claude only", "Any MCP client"],
["Windows WSL", "No", "Yes"], ["Windows WSL", "No", "Yes"],
@@ -413,16 +423,19 @@ export default function LivelinePage() {
<Section id="remote-access" title="Remote access"> <Section id="remote-access" title="Remote access">
<P> <P>
Control Chrome on a remote machine over the internet using tunnels. Control Chrome on any machine from anywhere over the internet.
Run the relay on the host, expose it through a tunnel, and connect The relay runs on the host alongside Chrome.
from anywhere. A{" "}
<A href="https://traforo.dev">traforo</A>{" "}
tunnel exposes it through Cloudflare, giving you a secure public URL.
No VPN, no firewall rules, no port forwarding.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
# On the host machine # On the host machine — start relay with tunnel
npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret> npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret>
# From anywhere # From anywhere — set env vars and use normally
export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev
export PLAYWRITER_TOKEN=<secret> export PLAYWRITER_TOKEN=<secret>
playwriter -s 1 -e "await page.goto('https://example.com')" playwriter -s 1 -e "await page.goto('https://example.com')"
@@ -430,9 +443,11 @@ export default function LivelinePage() {
<P> <P>
Also works on a LAN without tunnels &mdash; just set{" "} Also works on a LAN without tunnels &mdash; just set{" "}
<Code>PLAYWRITER_HOST=192.168.1.10</Code>. Use cases <Code>PLAYWRITER_HOST=192.168.1.10</Code>. Works for MCP
include controlling a headless Mac mini, providing remote user too &mdash; set <Code>PLAYWRITER_HOST</Code> and{" "}
support, and multi-machine automation. <Code>PLAYWRITER_TOKEN</Code> in your MCP client env config.
Use cases: headless Mac mini, remote user support,
multi-machine automation, dev from a VM or devcontainer.
</P> </P>
</Section> </Section>
+15 -6
View File
@@ -1,5 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (@media (prefers-color-scheme: dark));
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@@ -38,9 +38,8 @@
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.871 0.006 286.286); --sidebar-ring: oklch(0.871 0.006 286.286);
/* Dark mode overrides — @variant dark inside :root compiles to /* Dark mode overrides — uses prefers-color-scheme media query
:root:where(.dark, .dark *), following whatever strategy is via @custom-variant dark above */
configured in @custom-variant dark above */
@variant dark { @variant dark {
--background: oklch(0.21 0.006 285.885); --background: oklch(0.21 0.006 285.885);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
@@ -137,11 +136,21 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--color-muted); background: rgba(0, 0, 0, 0.15);
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: none; border: none;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-muted-foreground); background: rgba(0, 0, 0, 0.3);
}
@variant dark {
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
} }
+72 -70
View File
@@ -136,93 +136,95 @@ pre[class*="language-"] {
/* ====================================================================== /* ======================================================================
Dark mode - GitHub Dark inspired palette. Dark mode - GitHub Dark inspired palette.
Plain CSS (not processed by Tailwind), uses .dark ancestor selector Uses @variant dark from globals.css so the strategy is defined in
matching @custom-variant dark in globals.css. one place (@custom-variant dark in globals.css).
====================================================================== */ ====================================================================== */
.dark code[class*="language-"], @variant dark {
.dark pre[class*="language-"] { code[class*="language-"],
color: #e6edf3; pre[class*="language-"] {
} color: #e6edf3;
}
.dark .token.keyword, .token.keyword,
.dark .token.module, .token.module,
.dark .token.imports, .token.imports,
.dark .token.control-flow { .token.control-flow {
color: #ff7b72; color: #ff7b72;
} }
.dark .token.string, .token.string,
.dark .token.template-string, .token.template-string,
.dark .token.attr-value { .token.attr-value {
color: #a5d6ff; color: #a5d6ff;
} }
.dark .token.function, .token.function,
.dark .token.class-name { .token.class-name {
color: #d2a8ff; color: #d2a8ff;
} }
.dark .token.property, .token.property,
.dark .token.parameter { .token.parameter {
color: #ffa657; color: #ffa657;
} }
.dark .token.tag .token.tag { .token.tag .token.tag {
color: #7ee787; color: #7ee787;
} }
.dark .token.tag .token.class-name { .token.tag .token.class-name {
color: #7ee787; color: #7ee787;
} }
.dark .token.tag .token.attr-name, .token.tag .token.attr-name,
.dark .token.attr-name { .token.attr-name {
color: #79c0ff; color: #79c0ff;
} }
.dark .token.number { .token.number {
color: #79c0ff; color: #79c0ff;
} }
.dark .token.operator { .token.operator {
color: #ff7b72; color: #ff7b72;
} }
.dark .token.punctuation { .token.punctuation {
color: #e6edf3; color: #e6edf3;
} }
.dark .token.boolean, .token.boolean,
.dark .token.constant, .token.constant,
.dark .token.builtin { .token.builtin {
color: #79c0ff; color: #79c0ff;
} }
.dark .token.comment, .token.comment,
.dark .token.prolog, .token.prolog,
.dark .token.doctype, .token.doctype,
.dark .token.cdata { .token.cdata {
color: #8b949e; color: #8b949e;
font-style: italic; font-style: italic;
} }
.dark .token.tag .token.punctuation { .token.tag .token.punctuation {
color: #e6edf3; color: #e6edf3;
} }
.dark .token.spread { .token.spread {
color: #ff7b72; color: #ff7b72;
} }
.dark .token.plain-text { .token.plain-text {
color: #e6edf3; color: #e6edf3;
} }
.dark .token.regex { .token.regex {
color: #a5d6ff; color: #a5d6ff;
} }
.dark .language-bash .token.function { .language-bash .token.function {
color: #79c0ff; color: #79c0ff;
}
} }
+12 -8
View File
@@ -113,9 +113,10 @@
--ll-fade-12: rgba(255, 255, 255, 0); --ll-fade-12: rgba(255, 255, 255, 0);
} }
/* Dark mode overrides — plain CSS, not processed by Tailwind. /* Dark mode overrides — uses @variant dark from globals.css
Uses .dark ancestor selector matching @custom-variant dark in globals.css */ so the strategy is defined in one place */
.dark .liveline-page { @variant dark {
.liveline-page {
--ll-text-primary: rgba(255, 255, 255, 0.9); --ll-text-primary: rgba(255, 255, 255, 0.9);
--ll-text-secondary: rgba(255, 255, 255, 0.45); --ll-text-secondary: rgba(255, 255, 255, 0.45);
--ll-text-tertiary: rgba(255, 255, 255, 0.25); --ll-text-tertiary: rgba(255, 255, 255, 0.25);
@@ -161,6 +162,7 @@
--ll-fade-11: rgba(17, 17, 17, 0.002); --ll-fade-11: rgba(17, 17, 17, 0.002);
--ll-fade-12: rgba(17, 17, 17, 0); --ll-fade-12: rgba(17, 17, 17, 0);
} }
}
@supports (color: color(display-p3 1 1 1)) { @supports (color: color(display-p3 1 1 1)) {
.liveline-page { .liveline-page {
@@ -258,12 +260,14 @@
z-index: -1; z-index: -1;
} }
.dark .ll-inline-code { @variant dark {
color: rgba(255, 255, 255, 0.75); .ll-inline-code {
} color: rgba(255, 255, 255, 0.75);
}
.dark .ll-inline-code::before { .ll-inline-code::before {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
}
} }
/* ======================================================================== /* ========================================================================