Non-root tree items (visualLevel > 0) now use 4px vertical padding
instead of 2px, giving nested headings more breathing room while
keeping root-level items compact.
- Add FullWidth marker component that spans both content and aside grid
columns (lg:col-[2/-1]). Extracted at parse time like Aside/Hero.
FullWidth sections get my-5 vertical margin to match content rhythm.
- Fix auto-expand: clicking an expandable TOC item now expands it.
Previously only ancestors were expanded, not the item itself.
- TOC tree prefix characters use 1.2em font size with negative margin
so they don't inflate line height. Prefix indentation reduced to 1
char per level.
- Use FullWidth for the hero screenshot in index.mdx as demo.
- Add SVG ChevronIcon component for expandable items, shown on the right
- Toggle expand only on chevron click, not the whole row
- Remove width: fit-content from aside so it fills its column
- Add 6px margin between root-level TOC groups
- Use regular font weight (400) for all tree items
- Remove lowercase text-transform from tree items
- Remove line-height override from TocLink (inherits from parent)
Removed fontSize from the shared proseStyle object and added it to all
slot-main containers instead. Prose elements (P, OL, List) now inherit
font-size from their parent container rather than setting it explicitly,
so inside the aside they respect --type-toc-size (13px) instead of
overriding it with --type-body-size (14px).
- SectionRow aside and SidebarBanner now use a border instead of bg fill
- font size changed from --type-toc-size (13px) to --type-body-size (14px)
- line height aligned with prose style (1.6)
The 80ms setTimeout debounce blocked the input from reflecting keystrokes
while the TOC re-render was in progress. useTransition splits the work:
setQuery is urgent (input stays responsive), while setSearchState is
deferred and interruptible — new keystrokes cancel in-progress renders.
Input opacity drops to 0.5 during isPending as a subtle loading signal.
Replace flat TocItem/PreparedTocItem/TocGroup types with a recursive
TocTreeNode -> FlatTocItem pipeline that supports arbitrary page nesting.
**Types:**
- TocNodeType = 'page' | 'h1'-'h6' — heading level encoded in type field,
no separate headingDepth. Smaller objects, self-descriptive.
- TocTreeNode = { label, href, type, children } — recursive tree, the wire format.
- FlatTocItem = { label, href, type, visualLevel, prefix, parentHref, pageHref }
— flat rendering item with depth clamped to 0-3.
**Depth clamping:**
flattenTocTree() walks the recursive tree and clamps visual depth to max 4
levels (0-3). When pages nest deeply, headings flatten to siblings at level 3.
Formula: visualLevel = min(pageDepth + headingDepth, 3). Preserves correct
visual gap for skipped markdown levels (## -> ####) via the h-type encoding.
**Component changes:**
- TableOfContents accepts FlatTocItem[], renders with visibility filtering
instead of two-tier TocGroup. Expand/collapse works at any depth by
tracking expanded hrefs and computing visibility from parentHref chain.
- Search expand override merged into visibility computation so matched items
inside collapsed branches become visible during search.
- search.ts operates directly on FlatTocItem[] — removed SearchEntry and
buildSearchEntries(). Walks parentHref chain for ancestor expansion.
**Extraction:**
slugify, extractText, generateTocTree moved to toc-tree.ts (side-effect-free)
so they can be tested without importing the page module.
**Tests:**
21 vitest tests covering slugify, extractText, tree building (nested, skipped
levels, pop-back), flattening (prefixes, depth clamping, page wrapping,
parentHref/pageHref tracking), and full mdast -> tree -> flat pipeline.
**Remaining for multi-page:**
- Multi-page data source (multiple MDX files / manifest)
- Route-aware active tracking (pathname for page, IntersectionObserver for
headings on current page only)
- Cross-page navigation (SPA or full page load)
Add <Hero> component that gets auto-extracted from MDX at parse time
(same pattern as <Aside>) and rendered above the 3-column grid, aligned
with the center content column.
Layout changes:
- slot-page uses flex column with gap-[--layout-gap] for uniform spacing
between navbar, hero, and content grid
- --layout-gap (21px) is the single source of truth for both the flex gap
(flow spacing) and --sticky-top offset (viewport-relative sticky offset)
- Remove --content-top-gap variable and all its usage (spacer divs,
sidebar padding-top) — the flex gap handles all vertical spacing now
- Hero accepts arbitrary div props for extensibility
Hero extraction pipeline:
- isHeroNode() detects <Hero> mdxJsxFlowElement nodes in the mdast
- Hero nodes are filtered out before TOC generation and section grouping
- Rendered via RenderNodes through SafeMdxRenderer with full component map
- EditorialPage receives hero as a prop, renders it in a matching grid
(same column widths) so content aligns with the center column
- Sticky sidebar container gets maxHeight + flex column so TOC scrolls
when it exceeds viewport height
- Search input stays pinned at top (flexShrink: 0), nav scrolls below
- Nav gets paddingRight 4px for scrollbar gap
- Search input padding-y reduced from 4px to 2px
- Search-to-tree gap set to 12px
Adds a search input above the left sidebar table of contents powered by
Orama (in-memory, typo-tolerant, ~20μs per query).
**search.ts** — flat SearchEntry[] data model with parent/group references,
Orama DB creation, searchToc() that returns matched/dimmed/focusable hrefs
and group expand overrides. Reference: mintlify.com/oramasearch/orama/quickstart.md
**markdown.tsx** changes:
- TocLink: new props dimmed (opacity 0.3 + tabindex=-1), isHighlighted
(code-bg background), linkRef (scroll-into-view)
- TableOfContents: search input with F hotkey badge, 80ms debounced Orama
search, arrow key navigation through matched results, Enter navigates
to section anchor, Escape clears and blurs
- Groups containing matches auto-expand, non-matching items dimmed at 0.3
- IntersectionObserver scroll lock via ref to prevent activeId bouncing
during programmatic smooth scroll (scrollend + safety timeout)
- Global F key listener focuses search (skips input/textarea/contenteditable)
The built-in vite:asset load hook uses a regex that doesn't match when
query params get normalized to ?raw= (URLSearchParams.toString() adds
the =). This pre-load plugin handles ?raw imports for non-JS files
explicitly.
- Wrap subscribe, getSnapshot, getServerSnapshot in useCallback to prevent
React from tearing down and recreating the IntersectionObserver on every render
- Move window.location.hash read from render into subscribe, keeping render pure
- Add emit() helper that deduplicates ref writes and only notifies React on change
- Initialize activeRef with fallbackId; hash is read on first subscribe
Replaces useState + useEffect with a single useSyncExternalStore that:
- Initializes activeRef from window.location.hash to fix flash on first paint
- Uses IntersectionObserver in subscribe to track scroll position
- Returns fallbackId as server snapshot to prevent hydration errors
One less state hook (no useState, no useEffect for the observer).
No-op subscribe meant useSyncExternalStore was effectively a constant.
Replaced with a direct window.location.hash read in the useState
initializer, guarded by typeof window for SSR. One less hook.
Previously the TOC always initialized activeId to the first item's href,
causing a visible flash when the page loaded with a hash (e.g. #getting-started)
until the IntersectionObserver corrected it.
Now useLocationHash() reads location.hash synchronously during hydration via
useSyncExternalStore, so the first render already highlights the correct section.
The hook also subscribes to hashchange events for anchor link clicks.
The page layout grid previously used 5 explicit columns to embed spacing:
grid-cols: [210px] [50px] [520px] [50px] [210px]
TOC gap content gap sidebar
col1 col2 col3 col4 col5
The two gap columns (2 and 4) were real grid tracks, not CSS gap. This meant:
- Column references were off-by-two (sidebar = col 5, main = col 3)
- SectionRow subgrid spans referenced col-[3/-1] to skip the gap column
- slot-main had overflow:hidden to prevent content from bleeding into the
adjacent gap column — this later caused visible clipping of content
New layout uses 3 columns + CSS gap-x:
grid-cols: [210px] [520px] [210px] gap-x: 50px (--grid-gap)
TOC content sidebar
col1 col2 col3
Changes:
- markdown.tsx: grid template from 5 cols to 3 cols + lg:gap-x-(--grid-gap)
- SectionRow: col-[3/-1] → col-[2/-1], aside col-[3] → col-[2]
- inline subgrid (section-based layout): col-[3/-1] → col-[2/-1]
- flat layout slot-main: col-[3] → col-[2]
- globals.css: slot-sidebar-right grid-column 5 → 3
- globals.css: remove overflow:hidden from slot-main — no longer needed
since there is no adjacent gap column for content to bleed into
- AGENTS.md: update layout diagram to reflect the 3-column structure
and note that spacing comes from CSS gap-x, not a slot
--grid-max-width calc and --grid-gap variable are unchanged; the total
computed width (1040px) is identical.
Replace hardcoded standalone media queries with co-located @variant lg blocks so
base and responsive overrides live together in each selector.
Also make bleed tokens mobile-first (0 by default, enabled at lg) and remove the
now-redundant 650px bleed override in editorial.css.
Set Tailwind breakpoint token --breakpoint-lg to 1080px to match the editorial
grid geometry and keep utility and CSS responsive behavior aligned.
Two visual tweaks:
- Editorial layout top spacer: reduced from 80px to 60px in three places
(TOC paddingTop, grid spacer row, and flat-layout spacer div) so the
article content starts slightly higher on the page.
- Logo color: was hardcoded orange (#e8602c, matching the hero screenshot
gradient). Switched to var(--foreground) so the logo adapts to dark/light
theme instead of always rendering in orange. The old value is kept as a
comment for easy rollback if the orange look is preferred.
- Add min-width: 0, max-width: 100%, overflow: hidden on
.editorial-section-content to prevent content blowing out the grid
on mobile viewports
- Override to overflow: visible on desktop (1080px+) so that bleed
elements (negative-margin code blocks and images) render beyond
the 520px content column
- Adjust TOC paddingTop to 80px
Split the mdast tree at ## headings into sections. Each section becomes a
CSS subgrid row spanning columns 3-5. <Aside> JSX elements are extracted
from each section and rendered in the right sidebar column (col 5) with
position: sticky, anchored to the section's vertical extent.
Layout:
- TOC (col 1) spans all rows via grid-row: 1/span 100
- Section content (col 3) renders headings, paragraphs, code blocks
- Section aside (col 5) sticks at --sticky-top (81px) while scrolling
through its section, then scrolls away when the next section begins
- Mobile (<1080px): asides render inline as styled callout boxes
Other changes:
- Add --sticky-top CSS variable, used by both TOC and aside
- Add symmetric padding (16px top+bottom) to header navbar
- Add two example <Aside> blocks in index.md (Getting started, Accessibility snapshots)
Moved ::before shade from top:0 to top:81px (below the 44px logo row +
36px tab row + 1px border). Reduced height from 8rem to 6rem since it
no longer needs to cover the header area.
overflow:hidden on .editorial-grid broke position:sticky on the TOC sidebar.
Moved it to .editorial-grid-content which only clips the content column,
leaving the TOC free to stick.
- Collapsible TOC sidebar: sections start collapsed, auto-expand on scroll,
click to toggle. First section with children expanded by default.
- Mintlify-style tab bar: intro/examples/changelog tabs with 1.5px active
indicator and faux bold via text-shadow.
- Header row with logo above tabs, both sticky at top.
- CSS grid layout with named classes: 210px TOC | 50px gap | 520px content
| 50px gap | 210px mirror, centered via max-width + margin auto.
- Responsive: below 1080px collapses to single column, hides TOC/sidebar,
zeroes bleed variables, adds horizontal padding.
- Images capped at max-width: 100% on all breakpoints. PixelatedImage
container uses min(width, 100%). Bleed component gets overflow: hidden.
- SidebarBanner component for right gutter CTA cards.
- TabLink, TocLink extracted as reusable components.
Migrate from hand-written JSX components to MDX content rendered via safe-mdx.
Content now lives in src/content/index.md as standard markdown + JSX components.
- Parse MDX with mdxParse(), render with SafeMdxRenderer and component overrides
- Map markdown elements to editorial components: p→P, a→A, code→Code, ul→List, ol→OL, li→Li
- Headings via renderNode → SectionHeading with auto-slugified IDs
- Code blocks via renderNode → CodeBlock with Prism.highlight() (SSR-safe, no useEffect)
- TOC auto-generated from mdast headings instead of manual array
- Add Bleed component for image bleeds (--bleed-image: 48px)
- Fix code block alignment: --bleed: 44px so code text aligns with prose left edge
- PixelatedImage: make placeholder optional, skip fade-in when absent
- Reduce article gap 32px→20px, body font 15px→14px
- Add 12px padding-top above header logo bar
- Remove string-dedent dependency, add safe-mdx + @types/mdast
- Add 5-column CSS grid layout (TOC | gap | content | gap | sidebar)
- Add sticky header with logo, tab bar (Intro/GitHub/Changelog), and icon links
- Replace text logo with SVG logo using CSS mask-image + --logo-color variable (#e8602c)
- TOC: support multiple expanded sections, remove View Transition API, add toggle
- TOC: remove tree prefixes from root-level items, keep for nested children only
- TOC: sticky positioning with proper alignment to content column
- Increase TOC line height from 1.32 to 1.6
- Remove search bar from header
- Add SidebarBanner and TabLink components
- Add --logo-color CSS variable, remove --logo-filter dark mode hack
- Reduce --bleed from 44px to 16px
- Items collapsed by default, first section expanded on load
- Scroll-driven: active section auto-expands, previous clips away
- Uses document.startViewTransition() + flushSync for native browser
View Transition API (clip-path GPU-composited, zero layout thrashing)
- view-transition-class: toc-children shared across all section groups
- Clip-reveal from top on expand (180ms), clip-hide on collapse (140ms)
- Suppresses default root crossfade so only TOC children animate
- Restructured rendering into parent/children groups with TocLink component
- First TOC item active by default via useActiveTocId({ defaultId })
Make the landing page hierarchy easier to scan by rendering the sidebar like a compact terminal tree with nested headings and box-drawing guides. Slightly increase the editorial type scale while keeping the rail readable through tighter spacing, tuned contrast, and content-driven width.
All `-e` arguments in bash examples now use single quotes instead of double
quotes. This prevents bash from interpreting `$`, backticks, and backslashes
inside JS code — a major pain point for agents that construct playwriter
commands programmatically.
Changes across skill docs, README, and website copies:
- **Single quotes for one-liners**: `playwriter -s 1 -e 'await page.goto("...")'`
- **Heredoc preferred for multiline**: `<<'EOF'` disables all bash expansion
- **Quoting rules summary**: explains when to use single quotes vs heredoc vs `$'...'`
- **Rewrote mistake #6**: now explains *why* double quotes break (bash `$` expansion
silently corrupts regex like `/\$[\d.]+/` and template literals)
- **Added locator-scoped snapshot section**: shows `snapshot({ locator: page.locator("main") })`
which dramatically reduces output size from ~150 lines to ~20 lines by snapshotting
only a subtree instead of the full page
- **Added single-quote tip** to README quick start and SKILL.md stub
The locator-scoped snapshot feature already existed but wasn't documented.
Discovered during real-world Cloudflare dashboard automation where full page
snapshots were dominated by 60+ sidebar navigation links.
Closes#57
Highlights key differences: Playwriter uses your existing Chrome
(logins, extensions, cookies), can bypass captchas by disconnecting
the extension, lets you collaborate in the same browser window,
and gives full Playwright API + raw CDP access instead of a limited
shell command set.
Clarify recording guidance to prefer interaction-driven flows (click, hover, typing) so ghost cursor motion is visible during demos.
Tune ghost cursor movement defaults for faster, human-like motion and align cursor encoder script path handling with __dirname plus path.resolve conventions.
Introduce a first-class ghost cursor system for recordings and screenshots with namespace APIs (, ) while preserving backward-compatible top-level recording helpers.
Improve reliability after relay restarts by consolidating extension wait logic around , reducing false disconnected states during and execute flows.
Refactor recording glue into dedicated modules (, in ) to keep executor orchestration thin and make cursor lifecycle state explicit and testable.