Skip to content

feat: add captureShareImage util for element-to-OG-image capture - #6426

Draft
capJavert wants to merge 1 commit into
mainfrom
dev-take-pic
Draft

feat: add captureShareImage util for element-to-OG-image capture#6426
capJavert wants to merge 1 commit into
mainfrom
dev-take-pic

Conversation

@capJavert

@capJavert capJavert commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Captures any element (or React ref) via lazy-loaded snapdom and composes it onto a 1200x630 OG-sized PNG: contain-fit with theme-aware frame background and an optional branded bottom bar mirroring the app header (background-default fill, subtlest-tertiary top border, header logo rendered from LogoIcon/LogoText with theme color resolved at capture time). Dev builds expose window.captureShareImage for console testing.

Changes

Events

Did you introduce any new tracking events?

Experiment

Did you introduce any new experiments?

Manual Testing

Caution

Please make sure existing components are not breaking/affected by this PR

Preview domain

https://dev-take-pic.preview.app.daily.dev

Captures any element (or React ref) via lazy-loaded snapdom and composes
it onto a 1200x630 OG-sized PNG: contain-fit with theme-aware frame
background and an optional branded bottom bar mirroring the app header
(background-default fill, subtlest-tertiary top border, header logo
rendered from LogoIcon/LogoText with theme color resolved at capture
time). Dev builds expose window.captureShareImage for console testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@capJavert capJavert self-assigned this Aug 4, 2026
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Aug 4, 2026 1:47pm
storybook Building Building Preview Aug 4, 2026 1:47pm

Request Review

@capJavert

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @capJavert's task in 4m 21s —— View job


Review: captureShareImage util

  • Gather context (diff + base.css theme tokens, LogoIcon/LogoText, existing lib/blob.ts, lib/screenshot.ts)
  • Review captureShareImage.ts
  • Review devCaptureShareImage.ts + _app.tsx wiring
  • Check repo conventions
  • Post review

Solid, self-contained util — lazy import('@zumer/snapdom'), no barrel file, exported constants, fail-fast errors on unmounted/zero-size targets. A few things I'd fix before this gets wired into a product surface.


Theme resolution

1. resolveFrameBackground() first branch always wins, so the fallbacks are dead — and it can mismatch the branded bar (captureShareImage.ts:27-44)

base.css:1-2 sets html, #daily-companion-wrapper { background: var(--theme-background-default); }, so getComputedStyle(document.documentElement).backgroundColor is never rgba(0,0,0,0). The --theme-background-default and document.body fallbacks (lines 35-43) are unreachable.

That matters because of base.css:8-17 + MainLayout.tsx:166-172: while the v2 layout is active, html.layout-v2 at laptop+ paints a tinted background (color-mix(... --theme-surface-secondary 3% ...)). So the frame fills with the tint while drawLogoBar fills the bar with the raw --theme-background-default — a visible horizontal seam at the bar edge for any laptop+ v2 capture. Read the frame background and the bar background from the same source.

Fix this →

2. Reading theme from document.documentElement breaks in the companion (captureShareImage.ts:28, 58-66)

This lives in packages/shared, so the extension companion is a consumer. There, daily styles are scoped to #daily-companion-wrapper (base.css:1) and document.documentElement is the host page's <html> — no --theme-* vars at all. Every getPropertyValue returns '', so the bar background and border silently skip (if (barBackground) / if (barBorder)), and the logo color falls back to the host page's body color. Custom properties inherit, so getComputedStyle(element) on the capture target itself works for both surfaces and is a one-line change.

Fix this →

3. --theme-border-subtlest-tertiary is a color-mix() token, not a plain color (captureShareImage.ts:61-78)

base.css:248: --theme-border-subtlest-tertiary: color-mix(in srgb, var(--theme-border-subtlest-primary), transparent 80%). getPropertyValue substitutes the inner var() but does not evaluate color-mix() — you get the literal function string. Assigning an unparseable value to fillStyle is a silent no-op, leaving the previous fillStyle (the bar background) in place, so the top border just disappears. It happens to work on current evergreen browsers that accept color-mix() in canvas color parsing, but it's an invisible-failure path. Reading the used value off a probe element (e.g. getComputedStyle(el).borderTopColor after setting the class, or backgroundColor of a throwaway node with bg-border-subtlest-tertiary) always yields rgb()/rgba(). Same concern applies to any other token that's a color-mix (--theme-text-quaternary, overlays, etc.) if this grows.


Logo rendering

4. renderToStaticMarkup + string surgery on generated SVG is heavier and more brittle than needed (captureShareImage.ts:46-102)

Three coupled fragilities:

  • .replace('<svg ', ...) depends on the rendered markup having attributes after <svg. If LogoIcon ever renders bare <svg>, no width/height is injected and Firefox draws nothing from a viewBox-only SVG data URL — silent blank logo.
  • .replace(/var\(--theme-text-primary\)/g, color) is exactly the "don't run formatting regexes across already-generated HTML" pattern AGENTS.md calls out. It also only handles that one token — LogoText's --theme-actions-plus-default path (LogoText.tsx:39) would render as an unresolved var() if isPlus were ever passed.
  • LOGO_ICON_RATIO = 35 / 20 and LOGO_TEXT_RATIO = 77 / 20 duplicate the viewBox literals from LogoIcon.tsx:14 / LogoText.tsx:18. Change a viewBox and the logo skews with no test or type error.

Pulling react-dom/server into a client chunk (~100KB+ even browser-build) to stringify two static SVGs is a lot. Two lighter options: parse the viewBox out of the markup instead of hardcoding ratios, or skip SVG/data-URL/decode entirely and fill the paths with Path2D + context.translate/scale + globalAlpha for the fillOpacity={0.64} paths. The latter drops react-dom/server, the regexes, and image.decode() in one go.

Fix this →


Smaller items

5. instanceof HTMLElement fails cross-realm (captureShareImage.ts:108) — for the companion / any iframe-hosted element, HTMLElement is a different constructor and a valid element is treated as a ref, so target.current is undefined and it throws "not mounted". 'current' in target ? target.current : target is realm-safe.

6. Object URL never revoked (devCaptureShareImage.ts:24-30) — leaks a blob URL per call. Also, downloadUrl in packages/shared/src/lib/blob.ts:16 is the existing create-anchor-and-click helper; AGENTS.md says to search for an existing helper before writing one. It takes a URL rather than a Blob, so either reuse it or extract the shared bit rather than keeping two copies.

Fix this →

7. Caller-supplied snapdom options can silently break the fit math (captureShareImage.ts:114-141) — CaptureShareImageOptions extends SnapdomOptions, and ...snapOptions spreads after scale, so passing scale/width/height overrides the computed fit. Omit<SnapdomOptions, 'scale' | 'width' | 'height'> makes the contract explicit.

8. embedFonts: true on every call (captureShareImage.ts:138) — refetches/re-inlines fonts per capture. Worth passing snapdom's cache option if this ends up on a user-facing button.

9. No tests. packages/shared/src/lib has a *.spec.ts next to most modules. With @zumer/snapdom and canvas.getContext mocked, the cheap-but-real cases are: element vs ref target, unmounted ref throws, zero-size rect throws, branded: false skips the bar and re-centers content, and padding affecting the draw rect. That would also have caught #1 and #5.

10. Nitscontext.imageSmoothingQuality = 'high' still does a single-step downscale, so large elements (e.g. a 2000px-tall card contained into 486px) will alias; a mip-style two-pass downscale helps if quality matters. The bar always renders non-Plus LogoText, which diverges from the header for Plus users (fine if intentional). LOGO_BAR_BORDER = 2 vs the design system's 1px border is presumably deliberate at OG scale. And per AGENTS.md, captureShareImage is currently reachable only through the dev-only window global — fine as groundwork, just don't let it sit unused.

Bundle-wise the dev-only path looks OK: process.env.NODE_ENV === 'development' is folded by DefinePlugin and packages/shared declares sideEffects: ["*.css"], so the static import in _app.tsx:47 should tree-shake out of production. Worth confirming on the built output since it's the one place a dev-only util can leak into prod.

I did not run lint/tests/typecheck for this review — no changes to verify. Happy to implement any of the above if you want; just say which.
• branch dev-take-pic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant