Skip to content

Office fitted first-page/slide/sheet poster (declared poster slot) - #6176

Open
lukemelia wants to merge 5 commits into
mainfrom
cs-12527-office-fitted-first-pageslide-poster-poster-slot
Open

lukemelia wants to merge 5 commits into
mainfrom
cs-12527-office-fitted-first-pageslide-poster-poster-slot

Conversation

@lukemelia

@lukemelia lukemelia commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

What this does

DocxDef/PptxDef/XlsxDef share a poster screenshot slot (170×250 @ dsf 2, keyBy: 'file-content', useAsThumbnail). The capture-only component (file-formats/office-captures) renders the extracted structure's first unit as a page-shaped poster: a document's opening text flow, a deck's title slide, a workbook's first sheet.

  • Rasterization through the screenshot engine is the design, not a workaround: the browser has no OOXML decode path, so the extracted-structure rendering is the office viewer, and the capture engine shoots it. Deterministic by construction — synchronous DOM over already-extracted fields, no readiness signal needed.
  • Preview-less files capture the typed placeholder look (paper + format badge + structural count) rather than declining: the prerendered fitted cell serves the declaration-injected poster URL without knowing the capture outcome and its <img> has no error fallback, so a declined slot would leave the tile empty. Capturing the placeholder keeps the prerendered and live tiles agreeing.
  • The placeholder has one drawing. OfficePlaceholder (exported from file-formats/office-preview) is the paper, per-kind aspect, badge and structural count, rendered both by the fitted cell for an uncaptured file and by the capture's no-first-unit branch. Its colors read theme tokens with literal fallbacks, and the capture pins those tokens on its own wrapper, so a capture never follows the viewer's theme.
  • A titled document leads with its title. The synthetic heading (core title, else the filename) is drawn only when the extracted text flow carries no title block of its own.
  • No viewer changes: the fitted cell prefers the captured poster via the view model's thumbnail seam.

The video and 3D poster branches stack on this one.

Test plan

Realm-server e2e (extends the file-capture suite): writes the licensed docx-simple.docx, pptx-simple.pptx and xlsx-simple.xlsx fixtures, asserts each family's poster manifest entry and that the prerendered fitted HTML embeds the durable poster URL; writes an empty-body .docx and asserts the placeholder poster lands. CI-validated (the cached-fixture suite doesn't run locally).

Host integration (office-poster-capture-test): renders the capture component directly and pins which branch each extracted structure selects — no first unit, facts but no preview payload, a title-led flow, an untitled flow, deck and workbook first units, and a text-less first slide. Verified to discriminate: neutralizing the content gate turns the three placeholder tests red, and neutralizing the title-lead rule turns the title test red.

🤖 Generated with Claude Code

lukemelia and others added 3 commits September 17, 2026 13:53
DocxDef/PptxDef/XlsxDef share a poster screenshot slot whose capture-only
component renders the extracted structure's first unit — the document's
opening text flow, the deck's title slide, the workbook's first sheet — as
a page-shaped poster. Rasterization goes through the screenshot engine by
design: there is no in-browser OOXML decode path, so the extracted
rendering is the office viewer and the capture engine shoots it. The
render is synchronous DOM over extracted fields, so the engine's settle is
the readiness signal. keyBy 'file-content' + useAsThumbnail route it to
the fitted cell through the view model's thumbnail seam, with the typed
placeholder as the fallback.

Part of CS-12527.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…x/xlsx

The office poster capture always rendered — an oversize document the
extractor skipped, or a format with no preview payload, still produced a
filename-on-white page that persisted as `poster` and, through
`useAsThumbnail`, displaced the typed fitted placeholder (format badge +
count) in the thumbnail chain. Unlike the PDF sibling, it had no opt-out.

Gate the capture on a renderable first unit: `hasPosterContent` holds only
when the extracted structure carries document blocks, a titled/bulleted
first slide, or a first sheet with rows. When it does not, the component
keeps `data-screenshot-pending` standing so the engine's bounded wait
declines the slot and the typed placeholder stays the tile. The signal is
set once at the initial synchronous render off the getter — never flipped
from an async continuation — so the tracked-update caveat does not apply.

Fold the docx test into a per-family loop so the pptx (title slide) and
xlsx (first sheet) template branches are each exercised against the same
land-on-the-file-row contract, via the shared `writeAndSettle` helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GsYGsuCqHJz9GAti4jheG
…ecline

Declining the slot left the prerendered fitted tile an empty box: the
render pass injects meta.screenshots from the declaration, so the fitted
cell serves the poster URL regardless of capture outcome and its <img> has
no error fallback. Instead, the capture component's no-first-unit branch
now renders the same paper + format badge + structural count the fitted
placeholder draws (badge/count logic shared via office-preview exports),
so the always-served URL resolves to an informative tile and the
prerendered and live paths agree.

Covers the degenerate path with an in-test stored-zip OOXML fixture — an
empty-body document that parses as a valid package but yields no preview —
asserting the placeholder poster lands on the file row. Garbage bytes
would not exercise the branch: a FileContentMismatchError falls the file
back to plain FileDef, which declares no poster slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@lukemelia lukemelia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Reviewed this as a re-land: whether the three commits still hold against the code that moved underneath them (the roster merge and file-content gate in getScreenshots, the declaration-injected meta.screenshots in the render route, the screenshotsMeta thumbnail seam, and the fitted stage's <img>), plus the hasPosterContent gate and the placeholder-branch test. I did not re-review the PDF poster machinery or the capture engine.

No blocking issues. The change lands cleanly on current main, and the settled decision — capture the typed placeholder rather than decline the slot — is what the code does and what the header says. Three non-blocking asks.

  1. The placeholder e2e proves the slot lands, not that the placeholder branch rendered; pin the gate with a host render test — thread on declared-screenshots-file-test.ts.
  2. The placeholder paper is now drawn twice; one component is the follow-up — thread on the .placeholder CSS in office-captures.gts.
  3. Document poster: a synthetic filename heading sits above a title block that renders at body size — thread on the .page template in office-captures.gts.

PR text: the description's opening paragraph is delivery history; the current-shape rule for PR descriptions keeps only the note that the video and 3D poster branches stack on this one.

CI: Lint and all six realm-server shards are green on this head; host shards were still running when this was written and nothing in this diff touches host code.

Adjacent, out of scope: pdf-captures.gts's fail comment says an uncaptured poster URL is "a 404 the fitted cell's image fallback absorbs". FilePreviewStage renders generated-thumbnail with no error handler — the very fact this PR's header relies on — so the two sibling headers now contradict each other and the PDF one is the wrong one. For whoever touches the PDF capture next, not this PR.

});
}

test('an office file whose extraction yields no first unit captures the typed-placeholder poster', async function (assert) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Coverage, non-blocking: this pins "captures rather than declines", not the placeholder branch its name claims. Delete the hasPosterContent gate and the empty-body file takes the word branch — .page with the filename heading and zero blocks — which is still a PNG on the row and still the poster URL in the fitted HTML, so all four assertions stay green. The one thing the two branches differ on, what is in the pixels, is the one thing nothing observes.

The cheap pin is a host integration render of OfficePosterCapture next to the other content-only components in content-preview-components-test.gts: a plain { name: 'empty.docx' } model asserting .placeholder .ph-badge reads DOCX, and a model whose officeMetadata.previewJson carries one block asserting .page is present and .placeholder is not. That covers the gate in both directions without Chrome, and leaves this e2e its one real job — the slot must land.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Covered in 44bbaa8. office-poster-capture-test renders OfficePosterCapture directly and asserts which branch the extracted structure selects, in both directions: no metadata at all, metadata with facts but no preview payload, and a text-less first slide take the placeholder; a title-led flow, an untitled flow, a deck first slide and a workbook first sheet take their own first-unit branches.

It discriminates. Making hasPosterContent return true unconditionally turns exactly the three placeholder tests red (10 of 21 assertions), and restoring it returns the module to 21 of 21. This e2e keeps its own job — the slot must land on the file row — unchanged.

max-width: 48px;
}

/* The typed-placeholder branch, mirroring the fitted cell's uncaptured

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Follow-up, non-blocking: the badge and count strings are shared, but the paper is now drawn twice — .off-fitted / .paper* in office-preview and .ph-* here: the same 3:4 vs 4:3 aspect switch, the same ruled-lines and grid overlays, the same badge chrome, restated as literals. The "cannot drift" guarantee stops at the strings; the next tweak to the lined-paper overlay or the deck aspect lands on one copy.

One home would be a small placeholder-paper component (the markup and scoped CSS from office-preview, tokens keeping their literal fallbacks) that both the fitted branch and this capture render, with the capture wrapping it in a root that pins --border, --card, --fd-slate and the rest to the literals used here. That keeps the capture theme-independent — the reason this copy exists — without a second drawing. A follow-up, not this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Taken in 44bbaa8. OfficePlaceholder in office-preview is now the one drawing — paper, per-kind aspect, the ruled-line and grid overlays, badge and count — and both the fitted branch and the capture's no-first-unit branch render it. The .ph-* copy is gone.

The capture stays theme-independent without restating the drawing: the placeholder's colors read theme tokens with literal fallbacks, and the capture pins those tokens on its .placeholder wrapper. The badge and count helpers are module-private again, since the component is now the shared surface.

Comment on lines +159 to +160
<div class='page'>
<div class='page-title'>{{this.heading}}</div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Design nit, non-blocking: the document poster is the one branch that does not render the first unit as-is. It prepends a bold heading (core title, else the filename) that no first page carries, and a first block the extractor classified title falls through to body size beneath it — .page-block[data-style='heading'] is styled, data-style='title' is not — so a titled document reads as filename big, title small. The deck branch already treats heading as a fallback only. Suggest the same here: a [data-style='title'] rule at or above the heading size, and the synthetic heading only when the first block is not a title.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 44bbaa8, both halves. A [data-style="title"] rule now renders a title block at the heading's own size and weight, and the synthetic heading is drawn only when the first extracted block is not a title — the same fallback shape the deck branch already used.

Pinned by the title-led and untitled document cases in the new host test; forcing the heading to always render turns the title case red.

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   2h 54m 4s ⏱️ + 13m 13s
4 891 tests ±0  4 877 ✅ ±0  14 💤 ±0  0 ❌ ±0 
4 906 runs  ±0  4 892 ✅ ±0  14 💤 ±0  0 ❌ ±0 

Results for commit b2a9a24. ± Comparison against earlier commit 44bbaa8.

Realm Server Test Results

    1 files  ±0    241 suites  ±0   1h 23m 39s ⏱️ + 7m 54s
3 512 tests ±0  3 512 ✅ ±0  0 💤 ±0  0 ❌ ±0 
3 560 runs  ±0  3 560 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit b2a9a24. ± Comparison against earlier commit 44bbaa8.

… its title

Three review findings on the office poster slot.

The typed placeholder was drawn twice — once as the fitted cell's
`.off-fitted`/`.paper` in office-preview, once as the capture's `.ph-*`
copy in office-captures — with only the badge and count strings shared.
The paper shape, the per-kind aspect switch, the ruled-line and grid
overlays and the badge chrome were restated as literals, so the next
tweak to any of them would have landed on one copy. Both now render one
`OfficePlaceholder` component. Its colors read theme tokens with literal
fallbacks; the capture pins those tokens on its own wrapper, which is
what keeps a capture from following the viewer's theme without
redrawing it.

The document poster prepended a synthetic heading (core title, else the
filename) above a text flow that may open with its own title block, and
that block rendered at body size because only `heading` was styled. A
document whose first unit is a title now leads with it, styled as a
title; the synthetic heading stands in only when the flow carries none.

The poster capture had no test of its own: the realm-server e2e asserts
the slot lands on the file row, which both branches satisfy, so removing
the content gate left it green. A host integration test renders the
capture component directly and pins which branch each extracted
structure selects, in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lukemelia
lukemelia requested review from a team and FadhlanR September 17, 2026 19:40
@lukemelia
lukemelia added this pull request to stack #6181 September 17, 2026 19:40
@burieberry

Copy link
Copy Markdown
Contributor

Looking at these templates, I wonder if you have the latest skills. Pull the latest into the packages/skills-realm/contents and ask ai to update templates based on boxel skills.

The UI guidelines' token contract says a contract token never carries a
literal fallback, a plain color is set through background-color, and the
family's out-of-contract `--fd-*` tokens resolve once on a component root
rather than restating a literal at every use. These templates predated that
and did all three the old way.

Both now resolve `--fd-*` on their root — the badge onto `--tooltip`, the
one inverted pairing the theme guarantees — and read the contract bare
below. The viewer takes the theme's own ladders: the eyebrow role for the
tracked-out mono kickers, the label role for the sheet tabs, the spacing and
radius scales, the shadow scale for elevation, and nothing at all for
`.doc-title` / `.doc-heading`, which are an h1 and an h2 the container
already gives the heading roles to.

The poster capture keeps fixed values, since a capture keyed on file content
must not follow the viewer's theme — but it pins them once as tokens on the
capture root instead of scattering hex through the rules, so the drawing
reads the same tokens it would under a live theme, and its fallback partners
are pinned too so no chain can escape.

The format badge becomes one component rendered by the placeholder paper and
the viewer header alike, the same treatment the placeholder itself already
had. Sheet tabs gained the `aria-selected` their `role='tab'` implies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lukemelia

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] Pulled packages/skills-realm/contents (already at e777daf, whose head commit is the theming-guidance fix) and put both templates on what boxel-ui-guidelines now says, in b2a9a24.

What the skills changed here

  • No literal fallbacks on contract tokens. The token contract is explicit that every named token is re-declared at each themed-card boundary, so var(--card, #fff), var(--border, #d8d8d8), var(--muted-foreground, #555), var(--font-mono, ui-monospace, …) are dead weight. All gone.
  • The family's --fd-* tokens are outside the contract, so they now resolve once on each component root — the way the reference prescribes for a variable that may not exist — instead of restating a literal at every use. The inverted badge lands on --tooltip / --tooltip-foreground, the one inverted pairing the theme guarantees; it was hand-pairing --foreground with --card, which nothing guarantees.
  • background-color for plain colors, not the background shorthand.
  • The theme's own ladders in the viewer: the eyebrow role (as a whole group, per the reference) for the tracked-out mono kickers, the label role for the sheet tabs, --boxel-sp-*, --boxel-border-radius-*, and the shadow scale for the two elevations.
  • .doc-title / .doc-heading declare nothing. They are an h1 and an h2, and CardContainer already gives those the heading and section-heading roles — the contract calls re-declaring them a theme override.
  • 8cqw8cqi (inline-size container), a never-queried container-type dropped, and aria-selected on the tabs that already carried role='tab'.
  • One badge drawing. The format tag was drawn twice; it is now OfficeBadge, rendered by the placeholder paper and the viewer header alike — the same treatment the placeholder itself got earlier.

The capture is the one place that can't follow the theme, since the poster is keyed on file content and two viewers must get the same image. It keeps fixed values, but now pins them as tokens on the capture root and reads tokens below, instead of scattering hex through the rules. Statically checked: every token the capture subtree reads is pinned, including each fallback partner.

Verified against a dev stack, not just by eye — computed styles in a real render: badge 11px on --tooltip with --tooltip-foreground ink, the eyebrow tracking resolving to 0.88px, header padding to the --boxel-sp-2xs/-sm steps, .doc-title at the heading role's 22px, tabs at the label role's 12px, borders at --border. Nothing fell back to an unset variable. Acceptance | office file def 26/26 and Integration | office poster capture 21/21; packages/base lint clean.

Two judgment calls, flagged rather than silently taken:

  1. I stayed in the template-implementation layer (boxel-ui-guidelines) and did not rerun boxel-design's playbook on the visual language. This is file-type viewer chrome whose job is to show the extracted structure faithfully; a redesign is a different change from a skills-compliance pass. Say the word if you meant the visual pass too.
  2. The badges stay spans rather than Pill. Every renderer in file-formats uses plain elements and pulls in a boxel-ui component only for behavior, and the badge also renders inside a capture, where a themed component's own chrome would follow the viewer's theme.

One thing I could not bring into line: office-preview.gts sits at 48% <style scoped> against the 40% budget. The two drawings that actually repeated are each one component now; the rest is four unrelated renderings — text flow, slide faces, sheet grid, placeholder — in one file, so there is no duplication left to hoist. The file header records that rather than leaving it unexplained.

Comment on lines +193 to +201
/* A capture keyed on file content must come out the same for every
viewer, so the theme's tokens are pinned here, once, on the capture
root: the drawing below — and `OfficePlaceholder`, which renders into
the same root — reads them exactly as it does under a live theme, and
no viewer's theme can reach them. Each fallback partner is pinned
too, so no chain can escape to the viewer's theme if a token ahead of
it is ever dropped. The type scale is likewise held here rather than
scattered through the rules: these are the fixed proportions of a
170×250 page, not a themed ladder. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker but if it's NOT meant to be themed and if there's a valid reason for not using preexisting boxel variables from boxel-ui variables.css, I'd rather have local variable names used with --poster prefix so there's no confusion. The mix of px and rem values is also strange. We're saying the font sizes are scalable but not the margins or paddings. I'll make a note.

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.

2 participants