Skip to content

Add shared ViewerToolbar and EmbeddedReadCard across content viewers - #15038

Open
rtibbles wants to merge 4 commits into
learningequality:developfrom
rtibbles:shared-viewer-toolbar
Open

Add shared ViewerToolbar and EmbeddedReadCard across content viewers#15038
rtibbles wants to merge 4 commits into
learningequality:developfrom
rtibbles:shared-viewer-toolbar

Conversation

@rtibbles

@rtibbles rtibbles commented Jul 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Consolidate header components across multiple viewer components into a single ViewerToolbar component
  • Create EmbeddedReadCard that handles PDF/EPub preview in an embedded context

References

Finalizes / fixes #13824.

Reviewer guidance

Manual testing:

  • Use the embedded article example in the Kolibri QA channel
  • Ensure that in mobile format both PDF and EPUBs appear as non-interactive (but clickable) cards that open full screen
  • Ensure that the new toolbar works when embedded in desktop size
  • Ensure no regressions in regular HTML5 app, PDF, EPUB, and Bloom viewers
  • EmbeddedReadCard.vue:112 — active card sets pointer-events: none on its content to act as one click target. Confirm embedded content is still interactive outside card mode.
  • PdfRendererIndex.vue:31 — bespoke toolbars replaced by the shared ViewerToolbar across all four viewers. Confirm each viewer's actions (page nav, TOC, fullscreen) survived the swap.

Screenshots

The standalone ViewerToolbar is reproducible on this branch, verified live on the html5 and epub viewers.

The EmbeddedReadCard shots below come from the combined implementation PR #14548. The embedded PDF/EPUB preview needs the media-player migration to render the host article. It is not fully reproducible from this branch until #15035 and #15037 merge.

State Screenshot
Embedded PDF viewer (desktop) Embedded PDF
Embedded EPUB viewer (desktop) Embedded EPUB
Embedded PDF preview card (mobile) Mobile PDF
Embedded EPUB preview card (mobile) Mobile EPUB

PDF initial scale

Wrapping the template in EmbeddedReadCard moved this.$el onto the card's passthrough root. That root is display: contents, so clientWidth is 0. The initial fit computed scale = 0, and PdfPage fell back to :scale="scale || 1". Fixed by measuring the viewer element instead.

Reproduced at a 700px viewport. There showSideBar stays false, so its watcher never fires to recompute the scale. Left: scale 0, page collapsed. Right: scale 1.017, page fits the container.

PDF initial scale before and after

Not covered by a test. jsdom always reports clientWidth 0, so broken and fixed code behave identically there.

AI usage

Used Claude Code to consolidate the viewer toolbars into a shared ViewerToolbar, add EmbeddedReadCard, and split the change into reviewable commits. Verified with the ViewerToolbar, EmbeddedReadCard and TopBar Jest specs, prek, and the before/after capture above.

@rtibblesbot

rtibblesbot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🔵 Review posted

Last updated: 2026-07-30 21:41 UTC

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from e1b1c76 to f69fe2c Compare July 19, 2026 15:50
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 consolidates four bespoke viewer headers into a shared ViewerToolbar and adds EmbeddedReadCard — a clean, well-tested refactor that also improves RTL correctness. CI passing. Manual QA exercised all four toolbars (PDF/EPUB/HTML5/BloomPub) live with no PR-attributable console or a11y issues, but the EmbeddedReadCard active/mobile-embedded card state could not be rendered on this branch, so its interactive card path is unverified.

Heads up: the PR head advanced to f69fe2c after these passes ran; the three newest commits were not reviewed. Verdict is COMMENT, not approval.

Findings (see inline):

  • importantPdfRendererIndex non-embedded initial fit reads this.$el.clientWidth on a now-display:contents root → scale 0 → pages render at hardcoded 1.0 instead of fit-to-width.
  • suggestion — shared BaseToolbar restyle ripples into LessonMasteryBar (untouched consumer).
  • suggestionEmbeddedReadCard carries real click/keyboard logic but has no test.
  • suggestion — unrelated epubjs bootstrap changes (global.ePub drop, openAs) bundled into this PR.
  • nitpick — new components use Options API against the AGENTS.md setup() convention; unused commonCoreStrings mixin in EpubRendererIndex.

@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran a phased review pipeline over the pull request diff:

  • Classified the diff to select review passes (core, frontend, backend) and whether manual QA was required
  • Core review pass checked correctness, design, architecture, testing, completeness, and DRY/SRP/Rule-of-Three principles
  • Specialized frontend/backend review passes applied framework-specific lenses where those files changed
  • For UI changes: manual QA and an accessibility audit against a live dev server, when available
  • Checked CI status and linked issue acceptance criteria
  • Synthesized one review from those passes and chose the verdict from the findings, CI status, and QA evidence

}"
:style="{ backgroundColor: $themeTokens.text }"
@changeFullscreen="isInFullscreen = $event"
<EmbeddedReadCard

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

important: Making <EmbeddedReadCard> the template root changes what this.$el points at. In the normal non-embedded case the card renders in passthrough mode (.embedded-read-card-passthroughdisplay: contents), which generates no layout box, so this.$el.clientWidth is 0. The unchanged initial-fit line (this.scale = this.$el.clientWidth / (this.firstPageWidth * this.screenSizeMultiplier)) therefore computes scale = 0; PdfPage receives :scale="scale || 1", so every non-embedded PDF renders at a hardcoded 1.0 instead of fit-to-container, with no resize recompute to correct it. jsdom always reports clientWidth 0, so tests won't catch this, and QA didn't scale-check. Suggest reading the inner container that has a real box in both states, e.g. this.$refs.pdfContainer.$el.clientWidth (already used at the sidebar branch). The mobileEmbedded watcher below is fine — the card is a real box when active.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed. Scale now reads the viewer element via a viewerWidth() helper.

Not $refs.pdfContainer as suggested: it sits inside the v-else gated on firstPageHeight === null, so at the initial-fit line the DOM has not re-rendered and the ref is still null. $refs.pdfViewer.$el is the element $el resolved to before the card wrapped the template.

Reproducing it took some care — three things mask it:

  • the showSideBar watcher recomputes scale from pdfContainer, and fires whenever the value changes from its false initial
  • mounted() restores pdf_scale from localStorage, so an earlier good run poisons the next measurement
  • reopening the same hash URL does not remount the component

It only bites where showSideBar computes false, so the watcher never fires: small viewport, embedded, or an empty outline array. At 700px, scale is 0 and the page collapses to a blank sliver. With the fix, scale is 1.017 and the page fits.

PDF initial scale before and after

No test — jsdom reports clientWidth 0 either way, so broken and fixed are indistinguishable there.

padding-right: 24px;
padding-left: 24px;
z-index: 1;
min-height: 48px;

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

suggestion: BaseToolbar is shared; its only other consumer is LessonMasteryBar.vue, which this PR doesn't touch. Changing min-height (56→48px), padding (24→16px), and the box-shadow → %dropshadow-2dp reshapes the lesson mastery bar as a side effect. If intended for both, fine — otherwise scope the new dimensions to ViewerToolbar. Worth a QA glance at the lesson attempt view.

},
},
methods: {
emitRead() {

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

suggestion: EmbeddedReadCard isn't purely presentational — emitRead guards on active, onSpace early-returns and preventDefault()s, and role/tabindex/aria-label toggle by state. ViewerToolbar ships a spec but this component has none. Add at least a smoke test plus the active/inactive emit branches and the space-key handling.

* non-interactive, so the entire card acts as a single click target.
*/
export default {
name: 'EmbeddedReadCard',

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

nitpick: AGENTS.md says new components must use setup() (Composition API); both new files (EmbeddedReadCard, ViewerToolbar) use Options API. Consistent with the surrounding legacy viewer code, so low-priority — flagging only against the stated convention since these will be the template other viewers copy.

beforeMount() {
global.ePub = Epub;
this.book = new Epub(this.epubURL);
this.book = new Epub(this.epubURL, { openAs: 'epub' });

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

suggestion: Dropping global.ePub = Epub (+ its destroyed cleanup) and adding { openAs: 'epub' } are epubjs bootstrap behavior changes unrelated to the shared-toolbar scope. Removing the global looks safe (no other references), but openAs alters how books open. Consider splitting into its own commit and noting the rationale.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping both in this commit.

openAs is not a behavior change for our URLs. epubURL resolves to .../<hash>.epub, and epubjs determineType returns EPUB for a .epub extension (book.js:430), which is what open(url, openAs) would have derived anyway. It pins the type instead of re-sniffing the extension.

The global was write-only on our side, with no other references.

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.

That all holds up — pinning openAs: 'epub' yields the same type determineType would derive from the .epub extension, so it's a no-op for your URLs, and a write-only global is safe to drop. Rationale noted; no concern with keeping them together. Thanks for the detail.

SearchButton,
LoadingError,
},
mixins: [commonCoreStrings],

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

nitpick: commonCoreStrings is mixed in but never used here — the "READ" label lives inside EmbeddedReadCard. Drop the import and mixin unless a use is coming.

});
});

describe('custom components', () => {

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

praise: These custom components tests pin the new "components own their class" behavior precisely (first-render class preserved, safe-html not force-added, authored class forwarded via $attrs) — exactly the behavior-level coverage that guards against the earlier class-overwrite bug.

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review at f69fe2c7. No new commits and no thread replies since the last review, so every prior finding stands unchanged (code is identical). CI passing; manual QA found no blocking issues (the active EmbeddedReadCard and the SafeHTML .table-container change couldn't be exercised on this branch — author follow-ups).

7 prior findings; 0 resolved, 6 still open, 1 acknowledged (praise).

Still open:

  • importantPdfRendererIndex.vue: with <EmbeddedReadCard> now the template root, the non-embedded case renders the display:contents passthrough root, so this.$el.clientWidth is 0. The created() initial-fit (this.scale = this.$el.clientWidth / …) computes 0 and :scale="scale || 1" renders standard PDFs at native 1.0 instead of fit-to-width, with no resize watcher to recover. Measure this.$refs.pdfContainer.$el.clientWidth (the box-bearing ref already used in the showSideBar watcher); the mobileEmbedded watcher hits the same zero.
  • suggestionEmbeddedReadCard.vue: logic-bearing (active-guarded read emit, enter/space handling with preventDefault, conditional role/tabindex/aria-label) but has no spec, unlike its sibling ViewerToolbar. Add coverage for the emit branches and keyboard activation.
  • suggestionBaseToolbar.vue: the shared restyle (min-height 56→48, padding 24→16, dropshadow, z-index:1) also lands on the untouched LessonMasteryBar. Confirm intended for both, or scope the new dimensions to ViewerToolbar; worth a QA glance at the lesson attempt view.
  • suggestionEpubRendererIndex.vue: dropping global.ePub = Epub (+ its destroyed cleanup) and adding { openAs: 'epub' } are epubjs bootstrap changes unrelated to the shared-toolbar scope; consider splitting into their own commit with rationale.
  • nitpickEpubRendererIndex.vue: commonCoreStrings mixed in but unused now that the READ label lives in EmbeddedReadCard.
  • nitpick — both new shared components (ViewerToolbar, EmbeddedReadCard) use the Options API against AGENTS.md's setup() rule; consistent with surrounding legacy viewers, so low-priority, but these are the template others will copy.
Prior-finding status

UNADDRESSED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — non-embedded initial fit reads $el.clientWidth on display:contents root → scale 0
UNADDRESSED — packages/kolibri-common/components/EmbeddedReadCard.vue:77 — real click/keyboard logic but no test
UNADDRESSED — packages/kolibri-common/components/BaseToolbar.vue:41 — restyle ripples into untouched LessonMasteryBar
UNADDRESSED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:472 — unrelated epubjs global.ePub/openAs changes bundled in
UNADDRESSED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:245 — unused commonCoreStrings mixin
UNADDRESSED — packages/kolibri-common/components/EmbeddedReadCard.vue:51 — new components use Options API vs setup() convention
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise (no action)


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Ran the same phased review passes as a first review (core, frontend/backend lenses, manual QA when required)
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from f69fe2c to 908bea4 Compare July 19, 2026 23:22

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review at 908bea40.

All 7 prior findings resolved/acknowledged; 3 new findings on newly changed code (1 important, 2 suggestions). CI passing. Manual QA was required (UI changes) but did not run in this pass — the embedded/mobile card state is not reproducible on this branch, so the two SafeHTML visual changes and the EmbeddedReadCard active path are unverified visually.

  • importantEpubRendererIndex.vue: the sideBarOpen watcher can dereference an unmounted $refs.topBar. See inline.
  • suggestionSafeHTML/style.scss: the .table-container full-bleed removal is global to all SafeHTML tables. See inline.
  • suggestionSafeHTML/index.js: the new embedded-content-viewer wrapper branch has no spec. See inline.
Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — non-embedded initial fit read $el.clientWidth on display:contents root (now viewerWidth() via $refs.pdfViewer.$el)
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — shared-dimension side effect on LessonMasteryBar (dimensions now scoped to ViewerToolbar)
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — no tests (spec added, active/inactive + space/enter)
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:53 — Options API vs AGENTS.md (now setup())
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported-but-unused (removed)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:468 — openAs:'epub' + dropped global.ePub (author rationale accepted; no-op for .epub URLs)
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise: components-own-their-class tests (still present)


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

<LoadingScreen v-else-if="!loaded" />

<TopBar
v-if="!mobileEmbedded"

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

important: Gating TopBar with v-if="!mobileEmbedded" means $refs.topBar can be undefined when the sideBarOpen watcher runs. Path I can see statically: embedded + small viewport, tap the card → fullscreen (isInFullscreen true → mobileEmbedded false → TopBar mounts), open the TOC sidebar, press Esc — Esc both exits fullscreen (isInFullscreen false → mobileEmbedded true → TopBar unmounts) and fires @keyup.esc="closeSideBar". In the watcher's $nextTick, this.$refs.topBar.focusOnTocButton() (line 441) then throws a TypeError. The if (this.$refs.tocButton) guards inside TopBar.focusOn* don't cover this — the throw is on the absent topBar ref one level up. Can a sidebar be open (or transition closed) while mobileEmbedded is true? If so, guard the caller (this.$refs.topBar && this.$refs.topBar.focusOn…) or close open sidebars when leaving fullscreen.

width: calc(100% + 32px);
padding: 0 16px;
margin-left: -16px;
width: 100%;

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

suggestion: .table-container is the wrapper for every SafeHtmlTable, so dropping the calc(100% + 32px) / negative-margin full-bleed changes table layout for all SafeHTML consumers (QTI etc.), not just the embedded card this PR targets. The old trick let wide tables scroll edge-to-edge past the container padding. If the change is needed so embedded tables don't overflow the card, reasonable — please confirm the full-bleed removal is intended everywhere rather than a side effect of the embedded work.

mapChildren(node.childNodes),
);
// Wrap embedded ContentViewers in a layout container
if (component === 'ContentViewer') {

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

suggestion: This new embedded-content-viewer wrapper is a new structural branch in the render path with no accompanying spec, unlike the sibling custom-component and object-tag cases that are tested. A small render assertion (the ContentViewer element ends up inside .embedded-content-viewer) would lock the behavior.

}
},
// The component root is EmbeddedReadCard, which is `display: contents`
// when inactive and so has no box to measure; the viewer element always

@rtibblesbot rtibblesbot Jul 19, 2026

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.

Resolved — addressed in the current code.

praise: The display: contents measurement pitfall is documented exactly where a future reader would trip on it — this comment captures the subtle failure mode behind the earlier scale-0 bug and should prevent a regression.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from 908bea4 to 06653b2 Compare July 20, 2026 02:46

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — Delta re-review at 06653b2531. All prior findings resolved; no new findings. CI passing.

Verdict is COMMENT rather than APPROVE only because manual QA was required for this UI change but did not run in this pass — the UI was not visually verified here.

Prior-finding status

RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:438 — sideBarOpen watcher could deref unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal (reverted; scoped to embedded-content-viewer)
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec (spec added)
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — non-embedded initial fit read $el.clientWidth on display:contents root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — shared-dimension side effect on LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — no tests / Options API vs setup()
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported-but-unused
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:468 — openAs:'epub' + dropped global.ePub (author rationale accepted)
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise (no action)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:496 — praise on display:contents documentation (no action)


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from 06653b2 to e1ea8c7 Compare July 20, 2026 14:36

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review. All prior findings resolved or acknowledged; no new findings on the newly changed code. CI passing.

Commenting rather than approving because manual QA was required for this PR but did not run this pass — the active/mobile EmbeddedReadCard interactive state remains author-screenshot-verified only, not independently confirmed here.

Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — EmbeddedReadCard as template root changed this.$el / initial fit read clientWidth on display:contents root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle scoped to ViewerToolbar, does not ripple into LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — real click/keyboard logic now covered by spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:53 — now uses setup() per AGENTS.md convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — now uses setup() per convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported-but-unused removed
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher $refs.topBar deref guarded when TopBar unmounts
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — full-bleed removal scoped to embedded-content-viewer
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper now covered by spec
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:470 — openAs:'epub' + dropped global.ePub (author rationale accepted)
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — components-own-their-class tests (praise, no action)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:496 — display:contents documentation (praise, no action)


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

@rtibbles
rtibbles marked this pull request as ready for review July 21, 2026 15:11

@marcellamaki marcellamaki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So, just to be sure I'm following. The EmbeddedReadCard is a wrapper that turns the read-kind file preview into a button. clicking on it, at any place, opens it into a full screen? and from there, the learner can exit full screen, and they are essentially returned to where they were in the broader article context. so, it's.... almost but not precisely like a lightbox?

@marcellamaki marcellamaki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Richard and I discussed a bit more in person and looked at the figma spec together, and confirmed that my understanding of the expected behavior here aligned with both what i understood from the code and saw in the spec. therefore, :shipit:

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from e1ea8c7 to a7ca005 Compare July 24, 2026 21:47

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of a7ca0059.

All 12 prior findings resolved or acknowledged. CI green (55 pass / 17 skipped). Five new findings below, none blocking — an a11y gap on the active card, a layout artifact in the mobile-embedded EPUB state, and three nitpicks. Note that manual QA did not run for this review, so nothing here is visually verified; @marcellamaki's approval on this SHA stands.

Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el on a display:contents root / initial fit measurement
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — shared restyle rippling into LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:53 — Options API vs setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs setup() convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:470 — openAs: 'epub' + dropped global.ePub
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:496 — praise on the display:contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

@keydown.enter="emitRead"
@keydown.space="onSpace"
>
<div :class="active ? 'embedded-read-card-content' : 'embedded-read-card-content-passthrough'">

@rtibblesbot rtibblesbot Jul 24, 2026

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.

Resolved — addressed in the current code.

suggestion: The active card is a single pointer target, but its content stays in the tab order. pointer-events: none (line 99) covers mouse and touch; it does not remove descendants from the tab order or the a11y tree.

For the EPUB card this is reachable: TopBar is v-if'd off and the nav/bottom bars are v-show'd off, but the epubjs rendition is an <iframe> inside .epubjs-parent, which is implicitly focusable. A keyboard user tabbing past the card lands inside book content that is presented to AT as an opaque role="button" labelled "READ", and any Enter/Space pressed there bubbles back to the card's @keydown handlers and fires read.

inert on this wrapper when active covers pointer, keyboard and AT in one attribute, and lets you drop pointer-events: none:

<div
  :class="active ? 'embedded-read-card-content' : 'embedded-read-card-content-passthrough'"
  :inert="active || null"
>

.epub-viewer-content {
position: relative;
height: 100%;
height: calc(100% - #{$top-bar-height});

@rtibblesbot rtibblesbot Jul 24, 2026

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.

Resolved — addressed in the current code.

suggestion: Content height still subtracts the toolbar (and bottom bar) when neither is rendered. In the mobileEmbedded state TopBar is v-if="!mobileEmbedded" (line 22) and BottomBar is v-show="!mobileEmbedded", but $top-bar-height is still subtracted here and .navigation-and-epubjs still reserves bottom: $bottom-bar-height — roughly 100px of the 25vh card is dead space. This is the blank band under the "Introduction" box in the PR's own mobile-EPUB screenshot.

Relatedly, .top-bar-component { flex-shrink: 0 } (line 869) is a no-op: .epub-viewer is position: relative with no display: flex. Making .epub-viewer a column flex container and giving this rule flex: 1; min-height: 0 would let the layout self-adjust when the chrome is absent, and drop the hardcoded coupling to ViewerToolbar's min-height.

},
},
watch: {
mobileEmbedded() {

@rtibblesbot rtibblesbot Jul 24, 2026

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.

Resolved — addressed in the current code.

suggestion: This measures width synchronously; the sibling showSideBar watcher (line ~312) defers with $nextTick precisely because the grid has to re-render before clientWidth is meaningful.

On the exit-fullscreen transition mobileEmbedded flips to true and this runs before Vue re-renders EmbeddedReadCard into its active state — at which point the card adds padding: 16px, so viewerWidth() returns ~32px more than the box the pages actually land in and scale comes out slightly too large. Wrapping the body in this.$nextTick(...) matches the sibling watcher and measures post-layout.

bottom: 0,
};
}
if (this.embedded) {

@rtibblesbot rtibblesbot Jul 24, 2026

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.

Resolved — addressed in the current code.

nitpick: Neither html5_viewer nor bloompub_viewer declares css_selectors or allow_object_tag on its ContentRendererHook (only epub and pdf do), so neither can be selected by kolibri.canHandleElement() and embedded is always false here — this branch is currently dead. It also makes it easy to miss that BloomPubRendererIndex.containerStyle did not get the same branch despite the two files being otherwise near-identical. If the embedded path is intended for these viewers, the hooks need the selector declarations and bloompub needs the matching branch; if not, the embedded plumbing could be left out of both until then.


// Dimensions and elevation are scoped here rather than on the shared
// BaseToolbar, whose other consumer is sized for its own layout.
.viewer-toolbar {

@rtibblesbot rtibblesbot Jul 24, 2026

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.

Resolved — addressed in the current code.

nitpick: .viewer-toolbar overrides .base-toolbar at equal specificity — .base-toolbar sets min-height: 3.5rem and padding-left/right: 24px, both compile to one class plus a scope attribute, so the winner is stylesheet source order. It resolves in ViewerToolbar's favour today only because BaseToolbar is injected first as a dependency, which isn't something this component controls. .base-toolbar.viewer-toolbar { … } makes it deterministic for the same cost.

Also: ref="fullscreenButton" (line 17) is read by nothing here or in any consumer (it was already unused on the old epub TopBar it came from) — worth dropping unless a focusOnFullscreenButton follow-up is planned.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from a7ca005 to a257ebd Compare July 24, 2026 23:51

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of a257ebd835.

16 of 16 prior findings resolved or acknowledged; none still open. Two new suggestions and three nitpicks below, none blocking. CI passing; manual QA did not run in this pass, so nothing here is visually verified.

  • suggestion — the SafeHTML table-style "move" changes rendered output for every consumer (inline on SafeHtmlTable.vue)
  • suggestionshowSideBar watcher lost its ref guard on the way to viewerWidth() (inline on PdfRendererIndex.vue)
  • nitpick ×3 — hardcoded focus-outline colour; inert and scrollbar-gutter both sit above the browserslist floor (inline)
Prior-finding status

RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:16 — active card content stays in tab order / a11y tree
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:875 — content height subtracts chrome that isn't rendered; flex-shrink no-op
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured width synchronously
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue:122 — dead embedded branch
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue:77 — equal-specificity override vs .base-toolbar; unused ref="fullscreenButton"
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — EmbeddedReadCard as template root changes this.$el
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — shared with LessonMasteryBar.vue
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — not purely presentational (emitRead/onSpace guards)
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — new components must use setup()
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:470 — dropped global.ePub + { openAs: 'epub' }
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — unused commonCoreStrings mixin
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — $refs.topBar undefined when TopBar is v-if'd off
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — .table-container full-bleed rules dropped
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no coverage
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise, custom-component class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise, display: contents measurement pitfall documented


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

<style scoped>

/* Negative margins cancel the extra width, so wide tables scroll full-bleed. */
.table-container {

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

suggestion: The commit is framed as relocating styles, but this changes what renders for every SafeHTML table.

On develop the two rules competed: style.scss (global, unscoped) set width: calc(100% + 32px); padding: 0 16px; margin-left: -16px at specificity (0,1,0), while the scoped rule here compiled to .table-container[data-v-…] at (0,2,0) and won with just margin: 1em 0. So margin-left: -16px never applied — the container was 32px wider than its parent with no negative offset, overflowing to the trailing edge. Merging both into margin: 1em -16px makes the full-bleed offset live for the first time, for QTI assessment items and safe_html5 alike, not just the embedded card this PR targets.

Almost certainly the original intent, but worth saying so in the commit message and eyeballing a wide QTI table — nothing in SafeHtmlTable.spec.js can catch it (jsdom applies neither scoped nor imported stylesheets). The symmetric margin is also an RTL improvement: no RTLCSS flip needed where margin-left did.

@@ -281,11 +320,7 @@
},
showSideBar() {

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

suggestion: The previous revision guarded on this.$refs.pdfContainer && this.$refs.pdfContainer.$el before measuring. viewerWidth() (line ~502) has no equivalent bail-out — when neither $refs.recycleList nor $refs.pdfViewer resolves it returns 0, and this assigns scale = 0. :scale="scale || 1" then silently renders pages at native 1.0 — the failure mode this branch already fixed once, and equally invisible in jsdom. A null firstPageWidth yields NaN, which propagates into itemHeight and the scroller sizing.

The two sibling call sites both wrap the assignment in if (this.firstPageWidth); this one doesn't. I couldn't construct a reachable path (showSideBar only changes from the created() chain and toggleSideBar(), and $refs.pdfViewer exists in both), so this is defensiveness rather than a demonstrated bug — but the guard was cheap and the failure is silent.

}

.table-container:focus-visible {
outline: 3px solid rgb(51, 172, 245) !important;

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

nitpick: rgb(51, 172, 245) is a verbatim carry-over from style.scss, but it's now the only raw colour literal left in the repo's own SCSS/Vue, and AGENTS.md bans them. The token is $themeTokens.focusOutline (see kolibri/plugins/media_player/frontend/views/VideoPlayer.vue:23, bound via a CSS custom property). This component already reads this.$themePalette/this.$themeBrand in tableStyle, so binding it is a one-liner while the line is being touched.

The rule isn't dead despite .table-container having no tabindex — Chrome makes overflow: auto regions keyboard-focusable.

>
<div
:class="active ? 'embedded-read-card-content' : 'embedded-read-card-content-passthrough'"
:inert="active"

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

nitpick: inert is Chrome 102+ / Safari 15.5+ / Firefox 112+; packages/browserslist-config-kolibri/index.js targets Chrome >= 49, Safari >= 11.1, iOS >= 10, Samsung >= 5. Below those, only the pointer-events: none fallback applies — that covers pointer but leaves the wrapped viewer in the tab order and the a11y tree, exactly the gap the attribute closes. The comment on the fallback is honest about this; flagging only so the residual scope is explicit (a wicg-inert polyfill would be scope creep).

The Vue 2 side is correct: inert is in isBooleanAttr, so :inert="active" adds/removes the attribute rather than rendering inert="false", and the new specs pin that.

.pdf-container {
position: relative;
overflow-y: auto;
scrollbar-gutter: stable;

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

nitpick: scrollbar-gutter is Chrome 94+ / Firefox 97+ / Safari 18.2+, also above the browserslist floor. Where unsupported the comment's promise doesn't hold and measurement reverts to today's behaviour — graceful rather than broken, and moot on iOS where overlay scrollbars take no width. Worth knowing the fit is best-effort on older WebViews.

.epub-viewer {
position: relative;
// Counter-balance the padding to avoid unnecessary scroll
display: flex;

@rtibblesbot rtibblesbot Jul 25, 2026

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.

Resolved — addressed in the current code.

praise: The flex-column conversion removes the coupling rather than special-casing it — flex: 1; min-height: 0 on .epub-viewer-content deletes the calc(100% - $top-bar-height) hardcode that duplicated ViewerToolbar's min-height in a second file, and turns the pre-existing .top-bar-component { flex-shrink: 0 } from a no-op into a load-bearing rule. The layout now self-adjusts when TopBar is v-if'd off.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from a257ebd to bd79df2 Compare July 26, 2026 17:59

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — 23 of 23 prior findings resolved or acknowledged; none still open, and no new findings in this delta (5 added / 2 removed lines across two files).

Commenting rather than approving only because manual QA was required for this PR and did not run — no UI verification was performed in this round. CI passing.

Prior-finding status

RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:107 — full-bleed margin merge undocumented in the commit message
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:323 — showSideBar watcher lacked the sibling firstPageWidth guard
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:112 — raw rgb(51, 172, 245) literal instead of $themeTokens.focusOutline
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:743 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:858 — praise on the flex-column conversion
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:16 — active card left descendants in the tab order / a11y tree
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:875 — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue:122 — embedded branch unreachable / bloompub divergence
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue:77 — source-order-dependent override of .base-toolbar; unused fullscreenButton ref
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display:contents passthrough root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into the untouched LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:53 — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs the setup() convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:470 — openAs: 'epub' + dropped global.ePub (author rationale accepted)
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:496 — praise on the display: contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

@pcenov

pcenov commented Jul 27, 2026

Copy link
Copy Markdown
Member

Looks good to me, noticed the following minor issues:

  1. Epub reader: Clicking the table of contents menu opens the menu but clicking again on the icon doesn't close it as it used to do previously, so that's a regression:
epub.-.menu.not.closing.on.second.click.mp4
  1. The square icon of the 'Enter fullscreen' button is much smaller than the other icons so there's some sort of visual disbalance. Also previously we were showing just the square icon without the 'Enter fullscreen' text:
epub icons
  1. Pdf reader: The Bookmarks menu used to stretch all the way to the bottom of the page while now it's not:
pdf contents
  1. Embedded reader: The titles of the chapters are not centered between the slider and the content and the 'Enter fullscreen' button is blue:
embedded

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from bd79df2 to ab3e2b6 Compare July 27, 2026 23:31
@github-actions github-actions Bot added the DEV: backend Python, databases, networking, filesystem... label Jul 27, 2026
@rtibbles

Copy link
Copy Markdown
Member Author

All four of these issues should be resolved now.

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of ab3e2b61 (rebase onto develop, plus safe_html5_viewer enabled by default and amendments folded into d797bb8a).

All 23 prior findings resolved or acknowledged; 4 new findings below, all on newly changed code. CI still pending on this SHA, and manual QA did not run — this is a code-only review, so nothing here is a statement about how the viewers actually render.

  • important — stale :embedded binding left behind after ViewerToolbar dropped the prop (inline).
  • important — epub search side bar has no reachable close affordance from where focus lands (inline).
  • suggestion.pdf-viewer { height: 100% } doesn't resolve for the coach preview panel's max-height-only container (inline).
  • nitpick — unexplained visual/behavioural epub changes folded into the integration commit (inline).

Enabling safe_html5_viewer is correctly scoped: SafeHTML5AppAsset.presets is (KPUB_ZIP,) only, so no contention with HTML5AppAsset, and kolibri/plugins/utils/__init__.py:465 auto-enables it on upgrade.

Prior-finding status

RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — full-bleed margin merge undocumented in the commit message
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:321 — showSideBar watcher lacked the sibling firstPageWidth guard
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) literal instead of a theme token
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — active card left descendants in the tab order / a11y tree
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — embedded branch unreachable / bloompub divergence
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — source-order-dependent override of .base-toolbar
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display:contents passthrough root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into the untouched LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — Options API vs the setup() convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:744 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:444 — openAs: 'epub' + dropped global.ePub (author rationale accepted)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:837 — praise on the flex-column conversion
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise on the display: contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

<ViewerToolbar
class="pdf-controls-container"
:isInFullscreen="isInFullscreen"
:embedded="embedded"

@rtibblesbot rtibblesbot Jul 27, 2026

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.

Resolved — addressed in the current code.

important: ViewerToolbar no longer declares an embedded prop (dropped in this revision along with the labelled fullscreen button), and BaseToolbar declares no props either — so this binding falls through to the DOM and renders a stray embedded="true" attribute on .base-toolbar. This is the only surviving reference (grep -rn "<ViewerToolbar" -A6 across kolibri/plugins and packages returns just this one), so the refactor is one deletion away from complete.

</div>
</FocusLock>

<FocusLock

@rtibblesbot rtibblesbot Jul 27, 2026

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.

Resolved — addressed in the current code.

important: Removing the in-FocusLock SearchButton copy leaves keyboard users with no reachable way to close the search side bar from where focus lands.

  • The sideBarOpen watcher calls $refs.searchSideBar.focusOnInput(), so focus starts in the search <input>.
  • That input has @keyup.esc.stop (SearchSideBar.vue:15), which stops the Esc keyup before it reaches @keyup.esc="closeSideBar" on .epub-viewer-content (line 38).
  • The focus lock is active while searchSideBarIsOpen, so Tab can't reach the TopBar SearchButton.

Net: from the initial focus position neither Esc nor Tab-to-a-close-control works — the user has to Tab onto the submit button or a result first, then press Esc. TOC and settings are fine (Esc bubbles; SettingsSideBar has its own close button); search is the exception.

Cheapest fix is dropping .stop from SearchSideBar.vue:15 so Esc closes from the input as elsewhere; alternatively give SearchSideBar a titlebar close button matching SettingsSideBar. Worth checking why the .stop is there — it predates this PR (#14009) and carries no comment.

@extend %dropshadow-2dp;

position: relative;
height: 100%;

@rtibblesbot rtibblesbot Jul 27, 2026

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.

Resolved — addressed in the current code.

suggestion: height: 100% paired with display: block on the passthrough root restores sizing for consumers whose .content-viewer carries a definite height (ContentPage.vue:415, ContentArea.vue:65), but not where the consumer sets only max-height:

kolibri/plugins/coach/.../PreviewSelectedResources/PreviewContent.vue:94
  /deep/ .content-viewer { position: relative; max-height: 500px; }

With height: auto on the containing block a percentage height computes to auto (CSS 2.1 §10.5), so .pdf-viewer sizes to content and — the wrapper's overflow being visible — is no longer clipped at 500px the way it was on develop, where .content-viewer and .pdf-viewer were the same element. Strictly better than the previous display: contents state, so not a regression against the last reviewed SHA, but the coach resource-preview panel is worth a QA glance.

.bottom-bar {
height: 54px;
padding: 8px 8px 0;
padding: 8px 40px 0;

@rtibblesbot rtibblesbot Jul 27, 2026

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.

Resolved — addressed in the current code.

nitpick: d797bb8a ("Integrate shared viewer chrome across viewers") also carries changes that aren't toolbar integration and aren't mentioned in the body: this 8px 8px 08px 40px 0 absorption of the padding: 0 40px rule deleted from EpubRendererIndex's scoped block (right call — two same-specificity scoped rules whose winner depended on bundle order — but note the resulting vertical padding is the child's 8px 0, not the parent's 0 0); size="small" dropped from TocButton/SettingsButton/SearchButton, enlarging all three; and the handleMouseDown guard replacing the in-FocusLock button copies, which is behavioural. 9593597d shows your own bar for explaining a style merge — a sentence in d797bb8a's body would match it.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from ab3e2b6 to d69cf2e Compare July 28, 2026 00:45

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review at d69cf2e1. All 27 prior findings are resolved or acknowledged; 2 new low-severity notes on the delta (both inline).

CI on this SHA is still running (8 complete, no failures). Manual QA did not run in this pass, so none of the layout changes are visually verified here — the .embedded-read-card-passthrough flex change in particular can only be confirmed in a browser.

Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:35 — stale :embedded binding on ViewerToolbar
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:72 — search side bar had no reachable close affordance from initial focus
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:736 — .pdf-viewer { height: 100% } unresolved in a max-height-only container
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/BottomBar.vue:103 — ride-along epub changes undocumented in the integration commit
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — full-bleed margin merge undocumented
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:321 — showSideBar watcher lacked the firstPageWidth guard
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) instead of a theme token
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — active card left descendants in the tab order / a11y tree
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — dead embedded branch / bloompub divergence
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — source-order-dependent .base-toolbar override
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display: contents passthrough root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs the setup() convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:744 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:444 — openAs: 'epub' + dropped global.ePub
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:837 — praise on the flex-column conversion
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise on the display: contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

// The root carries the consumer's class (e.g. `.content-viewer`), so it must
// stay a real box for that sizing to apply. Flex column, not block, so the
// viewer fills a `max-height`-only consumer too.
.embedded-read-card-passthrough {

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

suggestion: This root is used for every non-mobile-embedded render of both the PDF and EPUB viewers, so blockflex column changes the containing-block model on all of their surfaces, not just the max-height: 500px coach preview that motivated it.

The two viewers also fill differently: .pdf-viewer uses flex: 1 (PdfRendererIndex.vue:735) while .epub-viewer keeps height: calc(100vh - 64px) (EpubRendererIndex.vue:839) and relies on default flex-shrink to be clamped. They agree in the 500px panel, but diverge in the consumers that set no height at all — ContentItem.vue:180 and .embedded-content-viewer (SafeHTML/style.scss:133): the EPUB stays at 100vh - 64px, the PDF sizes to content.

This is read off the CSS, not a browser — jsdom computes none of it, so no spec can guard it. Worth eyeballing the PDF and EPUB in the coach resource-preview panel, the channel renderer, and a SafeHTML embed before merge.

class="d-tc search-input"
type="search"
:aria-label="$tr('enterSearchQuery')"
@keyup.esc.stop

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

nitpick: Two small things on dropping .stop:

  1. 8ccb97c4's body enumerates three ride-along epub changes but not this fourth behavioural one — one more bullet would complete the set.
  2. This is type="search", where Blink/WebKit natively clear the field on Escape, so one press plausibly both clears searchQuery and closes the side bar. SearchSideBar is v-show-ed (EpubRendererIndex.vue:77) so it stays mounted — reopening would show an empty input next to the previous result count. I haven't confirmed Chrome's current Esc-clear behaviour in a browser; can you check what reopening search after Esc looks like?

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from d69cf2e to b4d5950 Compare July 28, 2026 02:06

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of b4d59504. 28 of 29 prior findings resolved or acknowledged; 1 still open (inline on SearchSideBar.vue), plus one new suggestion on the flex: 1 removal.

The only content delta since the last review is the removal of flex: 1 from .pdf-viewer (plus a fourth ride-along bullet in the integration commit body). That removal is correct — flex-basis: 0 would have overridden .pdf-embedded's height: 25vh. The residual question is what now sizes the viewer in the one max-height-only consumer.

CI on this SHA: 70 pass, 14 pending, 0 fail (Frontend tests and All file linting green). Manual QA did not run for this delta, so nothing here is visually verified — both open points are browser checks.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
Prior-finding status

UNADDRESSED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:15 — type="search" native Esc-clear on reopen
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:106 — passthrough blockflex column; PDF/EPUB fill divergence
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:35 — stale :embedded binding on ViewerToolbar
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:72 — search side bar had no reachable close affordance from initial focus
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:736 — .pdf-viewer { height: 100% } unresolved in a max-height-only container
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/BottomBar.vue:103 — ride-along epub changes undocumented in the integration commit
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — full-bleed margin merge undocumented
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:321 — showSideBar watcher lacked the firstPageWidth guard
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) instead of a theme token
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — active card left descendants in the tab order / a11y tree
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — dead embedded branch / bloompub divergence
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — source-order-dependent .base-toolbar override
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display: contents passthrough root
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into LessonMasteryBar
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs the setup() convention
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:744 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:444 — openAs: 'epub' + dropped global.ePub
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:837 — praise on the flex-column conversion
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise on the display: contents comment

@extend %dropshadow-2dp;

position: relative;
// No `flex: 1` — its `flex-basis: 0` would beat `.pdf-embedded`'s height.

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

suggestion: With flex: 1 gone, .pdf-viewer's only sizing input in a max-height-only consumer is height: 100%. The one such consumer is PreviewContent.vue:94 (/deep/ .content-viewer { max-height: 500px }), where the flex container's main size is indefinite — so the percentage computes to auto and the item is content-sized. What's left doing the work is default flex-shrink: 1 plus min-height: 0 clamping back to the 500px max-height, which only "fills" when the intrinsic content box exceeds 500px, and doesn't make the post-flex size definite for the height: 100% chain below (.full-height-container.scroller-height).

Worth a browser check: does the coach resource-preview panel still show a scrollable, correctly-clipped PDF rather than a page overflowing the 500px panel? If it does, the EmbeddedReadCard.vue:104-105 comment ("Flex column, not block, so the viewer fills a max-height-only consumer too") is now slightly ahead of the code — the mechanism is shrink-to-max-height, not grow-to-fill.

The removal itself is right for the embedded case; this is only about whether the case that motivated the flex column still holds. The comment on this line is the right thing to have written — flex: 1 looks like the obvious declaration here and the reason it isn't is invisible from the surrounding code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Checked it in the coach resource-preview panel. Still scrolls and clips correctly: .content-viewer 500 → .pdf-viewer 500 → .full-height-container 500 → #pdf-container 452, scrollHeight 203303.

PDF in the 500px panel
Same panel scrolled to 1200px

Toggling flex: 1 back on the live element gives identical numbers, so the removal is neutral here — this consumer never depended on it.

The comment was wrong though. The flex column does nothing for PDF; EPUB is what needs it, and by shrinking rather than filling: .epub-viewer's height: calc(100vh - 64px) measures 656px under a block passthrough at a 720px viewport, 500px under the flex column. Reworded to "so a taller viewer shrinks to a max-height-only consumer instead of overflowing it".

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.

The measured chain settles it — 500 → 500 → 500 with #pdf-container at 452 and a 203303 scrollHeight is a correctly clipped, scrollable panel, and identical numbers with flex: 1 toggled back on the live element confirms this consumer never leaned on it. Good catch that EPUB is the real motivating case: calc(100vh - 64px) resolving to 656px under a block passthrough at a 720px viewport is precisely the overflow the flex column absorbs, and 500px under it is shrink-to-fit doing the work. The reworded comment now matches the mechanism. Resolved.

type="search"
:aria-label="$tr('enterSearchQuery')"
@keyup.esc.stop
>

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

nitpick (still open): The commit-body half of this is resolved — 5e1253c5 now enumerates the ride-along epub changes. Still unanswered: on type="search", Blink/WebKit natively clear the field on Escape. Does one Esc now both empty searchQuery and close the v-show-ed side bar, leaving an empty input beside a stale result count on reopen? Purely a browser check — the new SearchSideBar.spec.js pins the Esc-propagates half, which is the part a test can hold.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed, and fixed. In Chromium one Esc does both: keydown → native clear → input (so v-model writes '' to searchQuery) → search → keyup reaches the ancestor's closeSideBar. The bar is v-showed, so reopening left an empty input beside the previous result count and list.

@keydown.esc.prevent on the input suppresses the native clear while the keyup still propagates, so the close affordance gained by dropping @keyup.esc.stop survives. Verified both halves against real event dispatch. The spec asserts defaultPrevented on the keydown — jsdom has no native clear to assert against.

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.

Thanks for running the real event dispatch — that chain (keydown → native clear → inputsearch, keyup still bubbling) is exactly the ordering that made this invisible from the source alone. @keydown.esc.prevent is the right lever: the native search-cancel is a keydown default action, so preventing it kills the clear without touching the keyup that reaches closeSideBar, and the close affordance you gained by dropping @keyup.esc.stop stays intact. Asserting defaultPrevented is the most a jsdom spec can pin here. Resolved.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from b4d5950 to 03b66a8 Compare July 28, 2026 03:15

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of 03b66a8 (17 lines across 3 files since b4d5950).

All 30 prior findings resolved or acknowledged; 2 new, non-blocking findings below, both first-raise observations on branch code earlier rounds didn't cover. CI on 03b66a8 is green on Frontend tests, All file linting, and Python tests (stage 1) (Morango integration jobs still running). Manual QA did not run this round, so nothing here is visually verified — the embedded-fullscreen point below wants one look in a browser before merge.

  • suggestion — embedded PDF keeps its light backdrop and hairline border in fullscreen (inline, PdfRendererIndex.vue:215)
  • nitpick:fullscreen { padding: 0 } is now a no-op after the flex-column conversion (inline, EpubRendererIndex.vue:847)
Prior-finding status

RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:17 — type="search" native Esc-clear on reopen
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:15 — .stop vs .prevent on the Esc handler
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:735 — what sizes .pdf-viewer after flex: 1 removal
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:736 — .pdf-viewer { height: 100% } unresolved in a max-height-only container
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:106 — passthrough block -> flex column; PDF/EPUB fill divergence
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:35 — stale :embedded binding on ViewerToolbar
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display: contents passthrough root
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:321 — showSideBar watcher lacked the firstPageWidth guard
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:72 — search side bar had no reachable close affordance from initial focus
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/BottomBar.vue:103 — ride-along epub changes undocumented in the integration commit
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — full-bleed margin merge undocumented
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) instead of a theme token
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — active card left descendants in the tab order / a11y tree
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — source-order-dependent .base-toolbar override
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into LessonMasteryBar
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — dead embedded branch / bloompub divergence
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:744 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:444 — openAs: 'epub' + dropped global.ePub
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:837 — praise on the flex-column conversion
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise on the display: contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

},
pdfViewerStyle() {
const style = {
backgroundColor: this.embedded ? this.$themeTokens.surface : this.$themeTokens.text,

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

suggestion: The backdrop keys off embedded alone, not embedded && !isInFullscreen. mobileEmbedded is false in fullscreen (line 211), so entering fullscreen from an embedded PDF — the flow EmbeddedReadCard's @read="$refs.pdfViewer.toggleFullscreen()" drives — lands on $themeTokens.surface instead of the $themeTokens.text backdrop a standalone fullscreen PDF gets, keeps the 1px solid fineLine hairline at the screen edge (line 219), and still matches .pdf-embedded, so /deep/ .pdf-page { margin: 2px auto } is the only thing separating pages. White pages on a white surface with a 2px gutter is where this could read badly — the dark backdrop is what gives page boundaries on develop, and CoreFullscreen's .normalize-fullscreen only forces sizing (CoreFullscreen.vue:82-87), so nothing resets the colour.

This is read off the CSS, not a browser. Worth one look at fullscreen-from-embedded (both the desktop toolbar button and the mobile card tap) before merge; if it's intended, the comment on line 218 is the natural place to say so.

@@ -838,27 +847,24 @@
padding: 0;

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

nitpick: These two selectors existed to cancel .epub-viewer's padding: 32px 24px, which the flex-column conversion above removed. With no padding to cancel, both are dead — as is .epub-viewer.small { padding: 0 } if it's still around for the same reason.

cursor: pointer;
}

// The root carries the consumer's class (e.g. `.content-viewer`), so it must

@rtibblesbot rtibblesbot Jul 28, 2026

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.

Resolved — addressed in the current code.

praise: Reworking this comment to the mechanism the measurements actually showed (shrink-to-fit, not fill) rather than leaving a plausible-but-wrong rationale in the code is the right call — same discipline as the @keydown.esc.prevent comment in SearchSideBar.vue:9.

@rtibbles

Copy link
Copy Markdown
Member Author

Not quite ready for QA rereview.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from 03b66a8 to 4decdb2 Compare July 29, 2026 19:52
@github-actions

Copy link
Copy Markdown
Contributor

npm Package Versions

Warning

The following packages have changed files but no version bump:

Package Version Changed files
kolibri 0.18.0 1

If these changes affect published code, consider bumping the version.

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of 4decdb23 (14 lines across the two renderer indexes). All prior findings are resolved or acknowledged; 4 new nitpicks below, none blocking.

Both open findings from the last round are fixed: the embedded PDF now gets the dark backdrop and drops its hairline border in fullscreen, and the dead .epub-viewer:fullscreen { padding: 0 } pair is gone along with the padding it cancelled. CI on this SHA is green on Frontend tests and All file linting; the only pending checks are the Morango integration jobs. Manual QA did not run in this pass, so nothing here is visually verified — hence COMMENT rather than APPROVE.

The new nitpicks are first-raises on branch code earlier rounds didn't reach (the .pdf-embedded class binding, the kpub preset label, some diff noise in the html5 viewer, and three unreachable ref guards in TopBar). All inline.

Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:215 — embedded PDF kept its light backdrop and hairline border in fullscreen
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:847 — :fullscreen { padding: 0 } a no-op after the flex-column conversion
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:17 — type="search" native Esc-clear on reopen
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:15 — .stop vs .prevent on the Esc handler
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:735 — what sizes .pdf-viewer after flex: 1 removal
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:736 — .pdf-viewer { height: 100% } unresolved in a max-height-only container
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:106 — passthrough blockflex column; PDF/EPUB fill divergence
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:35 — stale :embedded binding on ViewerToolbar
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — $el measured on the display: contents passthrough root
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:282 — mobileEmbedded watcher measured before re-render
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:321 — showSideBar watcher lacked the firstPageWidth guard
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:72 — search side bar had no reachable close affordance from initial focus
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — sideBarOpen watcher could deref an unmounted $refs.topBar
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — content height subtracted absent chrome in mobileEmbedded
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — commonCoreStrings imported but unused
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/BottomBar.vue:103 — ride-along epub changes undocumented in the integration commit
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — full-bleed margin merge undocumented
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) instead of a theme token
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — global .table-container full-bleed removal
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — embedded-content-viewer wrapper had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — active card left descendants in the tab order / a11y tree
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — click/keyboard logic had no spec
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — Options API vs the setup() convention
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — source-order-dependent .base-toolbar override
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — restyle rippling into LessonMasteryBar
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — dead embedded branch / bloompub divergence
ACKNOWLEDGED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — no css_selectors / allow_object_tag on the ContentRendererHook (informational)
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:103 — praise on reworking the comment to the measured mechanism
ACKNOWLEDGED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:744 — scrollbar-gutter below the browserslist floor (informational)
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:444 — openAs: 'epub' + dropped global.ePub
ACKNOWLEDGED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:837 — praise on the flex-column conversion
ACKNOWLEDGED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — praise on the components-own-their-class tests
ACKNOWLEDGED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — praise on the display: contents comment


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

:class="{
'pdf-controls-open': showControls,
'pdf-full-screen': isInFullscreen,
'pdf-embedded': embedded,

@rtibblesbot rtibblesbot Jul 29, 2026

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.

Resolved — addressed in the current code.

nitpick: The delta made the JS fullscreen-aware (embeddedInline = this.embedded && !this.isInFullscreen) but this class binding still keys off embedded, so the new .pdf-embedded block (lines 791–801 — develop applies the class but ships no rules for it) stays active in fullscreen-from-embedded.

Most of it is inert there: height: 25vh loses to CoreFullscreen's .normalize-fullscreen/.mimic-fullscreen height: 100% !important on the same element, and the nested .scroller-height repeats the base rule. The one live difference is /deep/ .pdf-page { margin: 2px auto } overriding PdfPage.vue:259's margin: 8px auto — so fullscreen reached from an embedded PDF separates pages by 2px where standalone fullscreen uses 8px. Cosmetic now that the backdrop matches, but if the intent is "fullscreen looks like standalone fullscreen", the class wants the same embeddedInline treatment the style got.

slideshow_manifest: 'Slideshow ({fileSize})',
slideshow_image: 'Slideshow image ({fileSize})',
bloompub: 'Bloom Pub Document ({fileSize})',
kpub: 'Kolibri Article ({fileSize})',

@rtibblesbot rtibblesbot Jul 29, 2026

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.

Resolved — addressed in the current code.

nitpick: The file header says these entries are the _READABLE strings from le_utils.constants.format_presets with ' ({fileSize})' appended. KPUB_ZIP_READABLE is Kolibri HTML5 Article; this is Kolibri Article.

The entry itself is correct and needed — format_presets.KPUB_ZIP is 'kpub', so getFilePresetString now resolves instead of hitting the logging.error fallthrough, which matters with safe_html5_viewer on by default. bloompub already deviates the same way, so there's precedent; worth a line only because the comment claims otherwise. Either match the readable name or note that the shorter label is deliberate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this — the file already deviates from le-utils elsewhere (bloompub vs Bloom Document), so the shorter label is in keeping with it.

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.

Understood — the shorter label is deliberate and consistent with the existing bloompub deviation. Deferring; the entry itself does the job of getting kpub out of the logging.error fallthrough.

import { now } from 'kolibri/utils/serverClock';
import CoreFullscreen from 'kolibri-common/components/CoreFullscreen';

import ViewerToolbar from 'kolibri-common/components/ViewerToolbar';

@rtibblesbot rtibblesbot Jul 29, 2026

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.

Resolved — addressed in the current code.

nitpick: Stray blank lines around this import (and inside components) that BloomPubRendererIndex.vue — same edit, same file shape — doesn't have. BloomPubRendererIndex.vue:58-80 conversely reorders the useContentViewer destructuring and return with no functional effect. Both lint-clean, both add diff noise to an otherwise mechanical swap.

*/
focusOnTocButton() {
this.$refs.tocButton.$el.focus();
if (this.$refs.tocButton) {

@rtibblesbot rtibblesbot Jul 29, 2026

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.

Resolved — addressed in the current code.

nitpick: These three ref guards can't be false — all three buttons render unconditionally inside ViewerToolbar's slots, with no v-if on any of them. The case that motivated the guard is one level up (TopBar itself is v-if="!mobileEmbedded"), and EpubRendererIndex.vue:413 already guards this.$refs.topBar before calling in. Dead branches read as if a state exists that doesn't.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from 4decdb2 to 625acce Compare July 29, 2026 21:35
@rtibbles

Copy link
Copy Markdown
Member Author

I think this is ready for QA rereview now.

@pcenov

pcenov commented Jul 30, 2026

Copy link
Copy Markdown
Member

@rtibbles I'm getting an error when trying to view the HTML Article with Embedded Files (in all supported browsers) so I can't verify the fixes on that front:

html article
index.js:395 TypeError: Cannot read properties of undefined (reading 'toString')
    at l.<anonymous> (SafeHtml5RendererIndex.vue:104:1)
    at Object.<anonymous> (kolibri.plugins.safe_html5_viewer.main-0.19.6.dev2369+g46b6e3206.js:4:30592)
    at o8 (kolibri.plugins.safe_html5_viewer.main-0.19.6.dev2369+g46b6e3206.js:4:28431)
    at i (kolibri.plugins.safe_html5_viewer.main-0.19.6.dev2369+g46b6e3206.js:4:28629)

The other previously reported issues are fixed.

rtibbles and others added 4 commits July 30, 2026 09:27
ViewerToolbar is a reusable viewer chrome bar built on BaseToolbar, for
adoption across the content viewers. EmbeddedReadCard frames embedded
read previews in a bordered card. SafeHTML now wraps embedded
ContentViewers in a layout container so they render consistently within
prose.
Replace the bespoke toolbars in the bloompub, epub, html5, and pdf
viewers with the shared ViewerToolbar, and adopt EmbeddedReadCard for
the embedded read preview in the epub and pdf viewers.

Four epub changes ride along:

- `BottomBar` absorbs the `0 40px` horizontal padding deleted from
  `EpubRendererIndex`'s scoped block — same-specificity scoped rules
  whose winner depended on bundle order. Vertical padding stays the
  child's `8px 0`.
- `TocButton`/`SettingsButton`/`SearchButton` drop `size="small"` to
  match the other toolbar buttons.
- The side bars lose their in-`FocusLock` toggle copies, so the top bar's
  toggles take the click; `handleMouseDown` now ignores events inside the
  top bar to stop close-then-reopen.
- `SearchSideBar`'s input swaps `@keyup.esc.stop` for
  `@keydown.esc.prevent`, so Esc closes the search side bar from the
  input as it already did for the others, without also firing the search
  field's native clear.
The component's scoped `margin: 1em 0` outranked the global
`.table-container` full-bleed, zeroing its negative margin but keeping the
+32px width, so every table overhung its container by 16px. SafeHtmlTable
is the sole renderer of the element; the two margin intents are merged.
KPUB_ZIP content has no renderer otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WAmANyoPp3rQdCNRdQt4k6
@rtibbles

Copy link
Copy Markdown
Member Author

Hrm, I'm not able to replicate this issue locally, either in the devserver, a local production build, or with the PEX asset downloaded from this PR.

My best guess is that it's another manifestation of this #15103 (which is being fixed in a parallel PR). I'll force push a rebase on latest develop just in case there's a weird issue with the build.

@rtibbles
rtibbles force-pushed the shared-viewer-toolbar branch from 625acce to f1be724 Compare July 30, 2026 19:22

@rtibblesbot rtibblesbot left a comment

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.

PR #15038 — delta re-review of the rebase since 625accee. 37 of 37 prior findings settled (36 resolved, 1 acknowledged); no prior finding remains open. git diff 625accee..HEAD over PR-scoped paths is empty, so the only new material is the rebase itself.

Two new suggestions inline (embedded card accessible name; persisted pdf_scale vs. the new fit watchers). Neither blocks.

CI is red on one leg only: Python unit tests for uv-supported Python versions (3.13) reports 349 failed, 3637 passed, 978 errors, all cascading from sqlite3.OperationalError: database table is locked at BEGIN IMMEDIATE in test setup. Every other leg on the same commit is green (3.6, 3.7, 3.10, Windows 3.8, macOS 3.10, postgres, all Morango legs, frontend, lint), and kolibri/plugins/utils/test/test_default_plugins.py — the test that would catch a bad DEFAULT_PLUGINS entry — passes inside the failing job. The only Python change on the branch is one line in default_plugins.py. Looks like a runner flake; worth a re-run to confirm before treating it as red.

The consolidation itself reads well: four bespoke fullscreen headers and four copies of the enterFullscreen/exitFullscreen $trs pair collapse into one component with one string namespace, and ViewerToolbar.spec.js pins each slot independently.

Not approving: manual QA was required for this PR but did not run (the dev server failed to start), so nothing here has been visually verified, and CI is not green.

Prior-finding status

RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:3 — EmbeddedReadCard as template root changes this.$el
RESOLVED — packages/kolibri-common/components/BaseToolbar.vue — shared with LessonMasteryBar, untouched consumer
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — not purely presentational (emitRead/onSpace guards)
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:57 — new components must use setup()
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:445 — dropped global.ePub + openAs: 'epub'
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — unused commonCoreStrings mixin
RESOLVED — packages/kolibri-common/components/SafeHTML/tests/SafeHTML.spec.js:191 — custom-components class-ownership tests
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:22 — $refs.topBar undefined when TopBar is v-if'd out
RESOLVED — packages/kolibri-common/components/SafeHTML/style.scss — dropping .table-container negative-margin sizing
RESOLVED — packages/kolibri-common/components/SafeHTML/index.js:166 — new embedded-content-viewer branch lacked coverage
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — display: contents measurement pitfall documented
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue — inert content vs. tab order
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — height subtracts unrendered toolbar/bottom bar
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:283 — synchronous width measure vs. deferred sibling watcher
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue — no css_selectors/allow_object_tag declared
RESOLVED — packages/kolibri-common/components/ViewerToolbar.vue — .viewer-toolbar overrides .base-toolbar at equal specificity
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue:104 — relocation commit changes rendered output
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:322 — dropped $refs.pdfContainer guard
RESOLVED — packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue — raw rgb(51, 172, 245) literal
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:18 — inert vs. browserslist floor
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:747 — scrollbar-gutter vs. browserslist floor
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:838 — flex-column conversion removes the coupling
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — ViewerToolbar no longer declares an embedded prop
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue:72 — removed in-FocusLock SearchButton copy
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:738 — height: 100% + display: block on passthrough root
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/BottomBar.vue:103 — non-toolbar changes riding along in d797bb8a
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:106 — block root for every non-mobile-embedded render
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:15 — dropping .stop
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:737 — .pdf-viewer sizing with flex: 1 gone
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue:17 — commit body now enumerates ride-along epub changes
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue — backdrop keys off embedded alone
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/EpubRendererIndex.vue — selectors cancelling .epub-viewer padding
RESOLVED — packages/kolibri-common/components/EmbeddedReadCard.vue:103 — comment reworked to the measured mechanism
RESOLVED — kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue:13 — .pdf-embedded keys off embedded, not embeddedInline
RESOLVED — kolibri/plugins/html5_viewer/frontend/views/Html5AppRendererIndex.vue:44 — stray blank lines / gratuitous reorder
RESOLVED — kolibri/plugins/epub_viewer/frontend/views/TopBar.vue:68 — unreachable $refs guards in focusOn*
ACKNOWLEDGED — packages/kolibri/components/internal/filePresetStrings.js:27 — kpub label deviates from KPUB_ZIP_READABLE


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

:class="active ? 'embedded-read-card' : 'embedded-read-card-passthrough'"
:role="active ? 'button' : null"
:tabindex="active ? 0 : null"
:aria-label="active ? readLabel : null"

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.

suggestion: Every card's accessible name is the bare word "Read".

aria-label is coreString('read') — the activity-type label — and the wrapped content is inert, so nothing else contributes to the name. A SafeHTML article with two embedded PDFs presents to a screen reader as two adjacent buttons both named "Read", with no way to tell which document each opens. The visible pill has the same problem, but sighted users have the clipped preview for context; AT users don't, precisely because the content is inert.

Cheapest fix: accept an optional label prop and have the viewer pass defaultFile.file_name. Alternatively compose Read {title} from the source element's title/aria-label, which SafeHTML/index.js:134-138 already forwards through attrs.

Suggestion rather than blocking because the single-embed case in the PR screenshots reads fine, and no acceptance criterion covers naming.

// preview state.
this.$nextTick(() => {
if (this.firstPageWidth) {
this.scale = this.viewerWidth() / (this.firstPageWidth * this.screenSizeMultiplier);

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.

suggestion: Does the persisted global pdf_scale clobber this fit?

handleUpdate (line ~484, unchanged) reasserts localStorage.pdf_scale on every update event where the recycle-list height is unchanged, and that key is global across all PDFs, not per-node:

if (localStorage.getItem('pdf_scale') != null) {
  this.setScale(parseFloat(localStorage.getItem('pdf_scale')));
}

So on a device where the user has ever zoomed a PDF, entering the card state looks like: this watcher sets the fit scale → heights change → one update takes the if branch → the next stable-height update takes the else branch and restores the persisted scale, undoing the shrink. Same reasoning applies to the new recycleListIsMounted re-fit below.

You already identified pdf_scale as a masking factor when reproducing the scale-0 bug, so you may have checked this — can you confirm the card preview still shrinks correctly with a non-default pdf_scale in localStorage? If not, scoping the restore to the non-embedded path is the smallest fix.

@radinamatic

Copy link
Copy Markdown
Member

I confirm I'm able to replicate the same issue as @pcenov, even with the latest asset after the forced push by @rtibbles. Same issue replicates in both Linux and Windows installs, in the app and all supported browsers.

Windows app Chrome Firefox
Win11 (start)  Running  - Oracle VirtualBox_051 Win11 (start)  Running  - Oracle VirtualBox_053 Win11 (start)  Running  - Oracle VirtualBox_052

Ubuntu22.4-logs.zip
Windows11-logs.zip

@radinamatic

Copy link
Copy Markdown
Member

Apologies, the above was completely my bad: I uploaded incorrectly formatted KPUB file during the last update of the QA channel. That error is now corrected and a follow up issue for the validation and denesting of KPUB files issue filed on Studio repo.

Revisiting the issues reported above by @pcenov using the updated KPUB with embeded files, all now appear fixed (re-tested in Windows app, all supported browsers and on the physical Android device):

  1. EPUB reader: ToC button opens/close on click ✔️
  2. EPUB reader: 'Enter fullscreen' without text ✔️
  3. PDF reader: Bookmarks side panel stretches all the way to the bottom ✔️
  4. Embedded reader: Chapter titles are centered, 'Enter fullscreen' button properly formatted ✔️

I bumped on a single issue while navigating pages of the complete-hummingbird-guide.epub in Firefox ONLY (no error in Chrome, Windows app or Android).

Win11 (start)  Running  - Oracle VirtualBox_054

logs.zip

@radinamatic radinamatic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Manual QA passes, one follow-up issue upcoming! :shipit:

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

Labels

DEV: backend Python, databases, networking, filesystem... DEV: frontend DEV: renderers HTML5 apps, videos, exercises, etc. SIZE: very large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable SafeHTMLViewer to leverage ContentViewer plugins

5 participants