From dfb45f1230572b2e99a2423da3cdcce47d9e99fa Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 3 Oct 2026 00:48:50 -0400 Subject: [PATCH] Add Playwright capture script for README images --- scripts/capture-screenshots.py | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 scripts/capture-screenshots.py diff --git a/scripts/capture-screenshots.py b/scripts/capture-screenshots.py new file mode 100644 index 0000000..4a7c0d8 --- /dev/null +++ b/scripts/capture-screenshots.py @@ -0,0 +1,90 @@ +"""Regenerates docs/img/* from base.html with Playwright (demo data only). + +Setup: pip install playwright pillow && playwright install chromium +Serve: mkdir /tmp/wt && cd /tmp/wt && ln -s web_template && ln -s /base.html base.html \ + && python3 -m http.server 8100 +Run: python3 scripts/capture-screenshots.py +""" +import asyncio, io +from PIL import Image +from playwright.async_api import async_playwright +import pathlib +OUT=str(pathlib.Path(__file__).resolve().parent.parent/'docs'/'img') +URL='http://127.0.0.1:8100/base.html' # serve so that /web_template/base.css and /base.html both resolve +TOP="document.activeElement&&document.activeElement.blur();document.documentElement.style.scrollBehavior='auto';window.scrollTo(0,0)" +HIDE = "document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('div').forEach(d=>{if(d.style.position==='fixed'&&d.style.bottom==='1rem'&&d.style.left==='1rem')d.style.display='none'})})" +async def hdr(pg,text): + h=pg.locator('.lt-section-header', has_text=text).first + return h.locator('xpath=..') if await h.count() else None +async def theme_run(theme): + async with async_playwright() as p: + b=await p.chromium.launch() + ctx=await b.new_context(viewport={'width':1440,'height':900}) + await ctx.add_init_script(HIDE) + await ctx.add_init_script(f"try{{localStorage.setItem('lt_theme','{theme}')}}catch(e){{}}") + pg=await ctx.new_page(); await pg.goto(URL); await pg.wait_for_timeout(5000) + await pg.evaluate(TOP); await pg.wait_for_timeout(500) + await pg.screenshot(path=f'{OUT}/{theme}-dashboard.png', full_page=True, clip={'x':0,'y':0,'width':1440,'height':900}) + if theme=='dark': + async def clipsec(text,name,extra=None,fixed=None): + h=pg.locator('.lt-section-header', has_text=text).first + if not await h.count(): print('missing',text); return + await h.scroll_into_view_if_needed() + hb=await h.bounding_box(); sy=await pg.evaluate('window.scrollY') + top=hb['y']+sy + if fixed: bot=top+fixed + else: + body=h.locator('xpath=following-sibling::*[1]'); bb=await body.bounding_box(); bot=bb['y']+sy+bb['height'] + await pg.screenshot(path=f'{OUT}/{name}.png', full_page=True, clip={'x':0,'y':top-4,'width':1440,'height':bot-top+8}); print('ok',name) + await pg.evaluate(TOP) + await clipsec('Ticket Queue','ticket-queue') + await clipsec('Component Reference','components',fixed=520) + await clipsec('Alert Banners','alerts') + await clipsec('SLA Banners','sla-banners') + await clipsec('Data Grid','data-grid') + await clipsec('Progress Bars','progress-bars') + await pg.evaluate(TOP); await pg.wait_for_timeout(300) + kb=pg.get_by_text('Kanban',exact=False).first + try: + await kb.click(timeout=3000); await pg.wait_for_timeout(900); await pg.evaluate(TOP); await pg.wait_for_timeout(500) + await pg.screenshot(path=f'{OUT}/kanban.png', full_page=True, clip={'x':0,'y':0,'width':1440,'height':900}); print('ok kanban') + except Exception as e: print('kanban fail',str(e)[:80]) + await pg.evaluate(TOP); await pg.keyboard.press('Control+k'); await pg.wait_for_timeout(500) + await pg.keyboard.type('ti',delay=80); await pg.wait_for_timeout(500) + await pg.screenshot(path=f'{OUT}/command-palette.png', clip={'x':320,'y':60,'width':800,'height':420}) + await b.close() +async def gif(): + frames=[]; + async with async_playwright() as p: + b=await p.chromium.launch() + ctx=await b.new_context(viewport={'width':1280,'height':720}) + await ctx.add_init_script(HIDE) + await ctx.add_init_script("try{localStorage.setItem('lt_theme','dark')}catch(e){}") + pg=await ctx.new_page(); await pg.goto(URL); await pg.wait_for_timeout(1200) + async def snap(n=1,ms=0): + if ms: await pg.wait_for_timeout(ms) + im=Image.open(io.BytesIO(await pg.screenshot())).convert('RGB').resize((960,540),Image.LANCZOS) + for _ in range(n): frames.append(im) + await snap(2,300) # boot sequence + await snap(2,1200); await snap(1,1500) + await pg.evaluate(TOP); await pg.wait_for_timeout(600); await snap(3) + await pg.keyboard.press('Control+k'); await snap(2,500) + for ch in 'new': + await pg.keyboard.type(ch); await snap(1,250) + await snap(3,300) + await pg.keyboard.press('Enter'); await snap(3,600) # toast + await pg.wait_for_timeout(1500) + await pg.evaluate("lt.theme.set('light')"); await snap(3,600) + await pg.evaluate("lt.theme.set('dark')"); await snap(2,600) + l=await hdr(pg,'Component Reference') + if l: + await l.scroll_into_view_if_needed(); await snap(3,500) + for y in (500,1000,1500): + await pg.mouse.wheel(0,500); await snap(2,500) + await b.close() + pal=[f.quantize(colors=96,method=Image.MEDIANCUT,dither=Image.NONE) for f in frames] + pal[0].save(f'{OUT}/demo.gif',save_all=True,append_images=pal[1:],duration=450,loop=0,optimize=True) + print('gif frames',len(frames)) +async def main(): + await theme_run('dark'); await theme_run('light'); await gif() +asyncio.run(main())