liveline: add Link component, OL, remove BackButton, dedent code blocks, spacing tweaks

- Add A (link) component with --ll-accent color, bright #58a6ff in dark mode
- Add OL (ordered list) component for numbered steps
- Remove BackButton from EditorialPage
- Use string-dedent for all CodeBlock strings, properly indented in source
- Getting started steps as OL with clickable Chrome Web Store link
- Star on GitHub link in intro paragraph
- Inline heading rule (flex + 1px line) replaces separate Divider
- Heading padding 24px top/bottom, article gap 32px
- Sidebar TOC item spacing increased to 5px
- Centered image captions
This commit is contained in:
Tommy D. Rossi
2026-02-20 12:53:21 +01:00
parent abd28162aa
commit acd63a9235
3 changed files with 353 additions and 277 deletions
+43 -2
View File
@@ -114,7 +114,7 @@ export function TableOfContents({
fontWeight: 475, fontWeight: 475,
lineHeight: "15.6px", lineHeight: "15.6px",
letterSpacing: "-0.04px", letterSpacing: "-0.04px",
padding: "3px 0", padding: "5px 0",
color: defaultColor, color: defaultColor,
fontFamily: "var(--ll-font-primary)", fontFamily: "var(--ll-font-primary)",
transition: "color 0.15s ease", transition: "color 0.15s ease",
@@ -249,6 +249,29 @@ export function Caption({ children }: { children: React.ReactNode }) {
); );
} }
export function A({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
color: "var(--ll-accent, #0969da)",
fontWeight: 600,
textDecoration: "none",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = "underline";
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = "none";
}}
>
{children}
</a>
);
}
export function Code({ children }: { children: React.ReactNode }) { export function Code({ children }: { children: React.ReactNode }) {
return ( return (
<code className="ll-inline-code"> <code className="ll-inline-code">
@@ -278,6 +301,25 @@ export function Section({ id, title, children }: { id: string; title: string; ch
); );
} }
export function OL({ children }: { children: React.ReactNode }) {
return (
<ol
className="m-0 pl-5"
style={{
fontFamily: "var(--ll-font-primary)",
fontSize: "14px",
fontWeight: 475,
lineHeight: "20px",
letterSpacing: "-0.09px",
color: "var(--ll-text-primary)",
listStyleType: "decimal",
}}
>
{children}
</ol>
);
}
export function List({ children }: { children: React.ReactNode }) { export function List({ children }: { children: React.ReactNode }) {
return ( return (
<ul <ul
@@ -564,7 +606,6 @@ export function EditorialPage({
textRendering: "optimizeLegibility", textRendering: "optimizeLegibility",
}} }}
> >
<BackButton />
<TableOfContents items={toc} logo={logo} /> <TableOfContents items={toc} logo={logo} />
<div <div
+108 -77
View File
@@ -4,11 +4,13 @@
* Styles from liveline.css and liveline-prism.css. * Styles from liveline.css and liveline-prism.css.
*/ */
import dedent from "string-dedent";
import "website/src/styles/liveline.css"; import "website/src/styles/liveline.css";
import "website/src/styles/liveline-prism.css"; import "website/src/styles/liveline-prism.css";
import { import {
EditorialPage, EditorialPage,
P, P,
A,
Code, Code,
Caption, Caption,
CodeBlock, CodeBlock,
@@ -16,6 +18,7 @@ import {
Section, Section,
PropsTable, PropsTable,
List, List,
OL,
Li, Li,
} from "website/src/components/markdown"; } from "website/src/components/markdown";
@@ -40,7 +43,8 @@ export default function LivelinePage() {
<P> <P>
Playwriter lets you control your Chrome browser with the full Playwriter lets you control your Chrome browser with the full
Playwright API. A Chrome extension, a local relay, and a CLI. No new Playwright API. A Chrome extension, a local relay, and a CLI. No new
browser windows, no Chrome flags, no context bloat. browser windows, no Chrome flags, no context bloat.{" "}
<A href="https://github.com/remorses/playwriter">Star on GitHub</A>.
</P> </P>
<ChartPlaceholder height={300} label="demo" /> <ChartPlaceholder height={300} label="demo" />
@@ -63,14 +67,19 @@ export default function LivelinePage() {
Three steps. Extension, icon click, then you&apos;re automating. Three steps. Extension, icon click, then you&apos;re automating.
</P> </P>
<CodeBlock lang="bash">{`# 1. Install the Chrome extension <OL>
# https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe <Li>
Install the{" "}
<A href="https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe">Chrome extension</A>
</Li>
<Li>Click the extension icon on a tab it turns green</Li>
<Li>Install CLI and run your first command:</Li>
</OL>
# 2. Click the extension icon on a tab — it turns green <CodeBlock lang="bash">{dedent`
npm i -g playwriter
# 3. Install CLI and run your first command playwriter -s 1 -e "await page.goto('https://example.com')"
npm i -g playwriter `}</CodeBlock>
playwriter -s 1 -e "await page.goto('https://example.com')"`}</CodeBlock>
<P> <P>
The extension connects your browser to a local WebSocket relay on{" "} The extension connects your browser to a local WebSocket relay on{" "}
@@ -79,10 +88,12 @@ playwriter -s 1 -e "await page.goto('https://example.com')"`}</CodeBlock>
leaves your machine. leaves your machine.
</P> </P>
<CodeBlock lang="bash">{`playwriter session new # new sandbox, outputs id (e.g. 1) <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await page.goto('https://example.com')" playwriter session new # new sandbox, outputs id (e.g. 1)
playwriter -s 1 -e "console.log(await snapshot({ page }))" playwriter -s 1 -e "await page.goto('https://example.com')"
playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock> playwriter -s 1 -e "console.log(await snapshot({ page }))"
playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
`}</CodeBlock>
<ChartPlaceholder height={200} label="getting started" /> <ChartPlaceholder height={200} label="getting started" />
<Caption> <Caption>
@@ -102,20 +113,22 @@ playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock>
back. back.
</P> </P>
<CodeBlock lang="bash">{`┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ <CodeBlock lang="bash">{dedent`
│ BROWSER │ │ LOCALHOST │ │ CLIENT │ ┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ │ │ │ BROWSER │ │ LOCALHOST │ │ CLIENT
│ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐ │ │ │ │ │
Extension │<───────┬───> :19988 │ │ │ CLI / MCP │ │ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐
│ └───────┬───────┘ │ WS │ │ └───────────┘ │ │ Extension │<───────┬───> :19988 CLI / MCP │
/extension └───────┬───────┘ │ WS │ │ │ └───────────┘
chrome.debugger │ │ │ v │ /extension │ │
v v ┌────────────┐ chrome.debugger │ v
│ ┌───────────────┐ │ │ /cdp/:id <───────────────>│ │ execute │ │ v │ │ v │ │ ┌────────────┐
│ Tab 1 (green) │ └────────────────────── WS │ └────────────┘ ─────────────── │ │ /cdp/:id <───────────────>│ │ execute │
│ │ Tab 2 (green) │ │ │ │ │ │ Tab 1 (green) │ │ └──────────────────────┘ WS │ └────────────┘
│ │ Tab 3 (gray) Tab 3 not controlled │ Playwright API │ │ Tab 2 (green) │ │ │
└─────────────────────┘ (extension not clicked) └─────────────────┘`}</CodeBlock> │ │ Tab 3 (gray) │ │ Tab 3 not controlled │ Playwright API │
└─────────────────────┘ (extension not clicked) └─────────────────┘
`}</CodeBlock>
<P> <P>
No Chrome restart required. No <Code>--remote-debugging-port</Code>{" "} No Chrome restart required. No <Code>--remote-debugging-port</Code>{" "}
@@ -135,14 +148,16 @@ playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock>
to every element. to every element.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "await snapshot({ page })" <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await snapshot({ page })"
# Output: # Output:
# - banner: # - banner:
# - link "Home" [id="nav-home"] # - link "Home" [id="nav-home"]
# - navigation: # - navigation:
# - link "Docs" [data-testid="docs-link"] # - link "Docs" [data-testid="docs-link"]
# - link "Blog" role=link[name="Blog"]`}</CodeBlock> # - link "Blog" role=link[name="Blog"]
`}</CodeBlock>
<P> <P>
Each line ends with a locator you can pass directly to{" "} Each line ends with a locator you can pass directly to{" "}
@@ -151,11 +166,13 @@ playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock>
<Code>search</Code> to filter large pages. <Code>search</Code> to filter large pages.
</P> </P>
<CodeBlock lang="bash">{`# Search for specific elements <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })" # Search for specific elements
playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })"
# Always print URL first, then snapshot — pages can redirect # Always print URL first, then snapshot — pages can redirect
playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"`}</CodeBlock> playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"
`}</CodeBlock>
<P> <P>
Snapshots are text. They cost a fraction of what screenshots cost in Snapshots are text. They cost a fraction of what screenshots cost in
@@ -179,10 +196,12 @@ playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).th
read the labels, click by reference. read the labels, click by reference.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })" <CodeBlock lang="bash">{dedent`
# Returns screenshot + accessibility snapshot with aria-ref selectors playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })"
# Returns screenshot + accessibility snapshot with aria-ref selectors
playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock> playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
`}</CodeBlock>
<P> <P>
Labels are color-coded by element type: yellow for links, orange for Labels are color-coded by element type: yellow for links, orange for
@@ -208,15 +227,17 @@ playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"`}</CodeBlock>
different state. Browser tabs are shared. different state. Browser tabs are shared.
</P> </P>
<CodeBlock lang="bash">{`playwriter session new # => 1 <CodeBlock lang="bash">{dedent`
playwriter session new # => 2 playwriter session new # => 1
playwriter session list # shows sessions + state keys playwriter session new # => 2
playwriter session list # shows sessions + state keys
# Session 1 stores data # Session 1 stores data
playwriter -s 1 -e "state.users = await page.$$eval('.user', els => els.map(e => e.textContent))" playwriter -s 1 -e "state.users = await page.$$eval('.user', els => els.map(e => e.textContent))"
# Session 2 can't see it # Session 2 can't see it
playwriter -s 2 -e "console.log(state.users)" # undefined`}</CodeBlock> playwriter -s 2 -e "console.log(state.users)" # undefined
`}</CodeBlock>
<P> <P>
Create your own page to avoid interference from other agents. Reuse Create your own page to avoid interference from other agents. Reuse
@@ -224,10 +245,12 @@ playwriter -s 2 -e "console.log(state.users)" # undefined`}</CodeBlock>
fresh one, and store it in <Code>state</Code>. fresh one, and store it in <Code>state</Code>.
</P> </P>
<CodeBlock lang="bash">{`playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')" <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')"
# All subsequent calls use state.myPage # All subsequent calls use state.myPage
playwriter -s 1 -e "console.log(await state.myPage.title())"`}</CodeBlock> playwriter -s 1 -e "console.log(await state.myPage.title())"
`}</CodeBlock>
</Section> </Section>
@@ -239,14 +262,16 @@ playwriter -s 1 -e "console.log(await state.myPage.title())"`}</CodeBlock>
without reloading. without reloading.
</P> </P>
<CodeBlock lang="bash">{`# Set breakpoints and debug <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()" # Set breakpoints and debug
playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))" playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })" playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))"
playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })"
# Live edit page code # Live edit page code
playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()" playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()"
playwriter -s 1 -e "await state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"`}</CodeBlock> playwriter -s 1 -e "await state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"
`}</CodeBlock>
<P> <P>
Edits are in-memory and persist until the page reloads. Useful for Edits are in-memory and persist until the page reloads. Useful for
@@ -270,15 +295,17 @@ playwriter -s 1 -e "await state.editor.edit({ url: 'https://example.com/app.js',
<Code>state</Code> and analyze across calls. <Code>state</Code> and analyze across calls.
</P> </P>
<CodeBlock lang="bash">{`# Start intercepting <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "state.responses = []; page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } })" # Start intercepting
playwriter -s 1 -e "state.responses = []; page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } })"
# Trigger actions, then analyze # Trigger actions, then analyze
playwriter -s 1 -e "await page.click('button.load-more')" playwriter -s 1 -e "await page.click('button.load-more')"
playwriter -s 1 -e "console.log('Captured', state.responses.length, 'API calls'); state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)))" playwriter -s 1 -e "console.log('Captured', state.responses.length, 'API calls'); state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)))"
# Replay an API call directly # Replay an API call directly
playwriter -s 1 -e "const data = await page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url); console.log(data)"`}</CodeBlock> playwriter -s 1 -e "const data = await page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url); console.log(data)"
`}</CodeBlock>
<P> <P>
This is faster than scrolling through DOM. Capture the real API This is faster than scrolling through DOM. Capture the real API
@@ -298,15 +325,17 @@ playwriter -s 1 -e "const data = await page.evaluate(async (url) => { const res
as MP4. as MP4.
</P> </P>
<CodeBlock lang="bash">{`# Start recording <CodeBlock lang="bash">{dedent`
playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })" # Start recording
playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })"
# Navigate, interact — recording continues # Navigate, interact — recording continues
playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')" playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')"
playwriter -s 1 -e "await page.goBack()" playwriter -s 1 -e "await page.goBack()"
# Stop and save # Stop and save
playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"`}</CodeBlock> playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"
`}</CodeBlock>
<P> <P>
Unlike <Code>getDisplayMedia</Code>, this approach Unlike <Code>getDisplayMedia</Code>, this approach
@@ -383,13 +412,15 @@ playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page
from anywhere. from anywhere.
</P> </P>
<CodeBlock lang="bash">{`# On the host machine <CodeBlock lang="bash">{dedent`
npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret> # On the host machine
npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret>
# From anywhere # From anywhere
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')"`}</CodeBlock> playwriter -s 1 -e "await page.goto('https://example.com')"
`}</CodeBlock>
<P> <P>
Also works on a LAN without tunnels &mdash; just set{" "} Also works on a LAN without tunnels &mdash; just set{" "}
+4
View File
@@ -79,6 +79,9 @@
rgba(0, 0, 0, 0.04) 0px 4px 16px, rgba(0, 0, 0, 0.04) 0px 4px 16px,
rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset; rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset;
/* Link accent color */
--ll-accent: #0969da;
/* P3 accent colors (wider gamut) */ /* P3 accent colors (wider gamut) */
--ll-primary: #3e9fff; --ll-primary: #3e9fff;
--ll-secondary: #f09637; --ll-secondary: #f09637;
@@ -130,6 +133,7 @@
rgba(255, 255, 255, 0.02) 0px 4px 16px, rgba(255, 255, 255, 0.02) 0px 4px 16px,
rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset; rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset;
--ll-accent: #58a6ff;
--ll-primary: #60a5fa; --ll-primary: #60a5fa;
--ll-secondary: #f5a623; --ll-secondary: #f5a623;