From b7aaf211c9f7d1c9461c710e6a6b9ddf24c7adad Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Wed, 11 Nov 2026 19:40:31 +0100 Subject: [PATCH 1/3] fix(rq-qz71): A spoken line's speaker name row counts as words, so no dot stands under it Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WCDKexRotFmmxHZ234zGww --- web/src/components/lantern/AreaNames.test.ts | 20 +++++++++++++++++++- web/src/components/lantern/AreaNames.vue | 5 +++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/web/src/components/lantern/AreaNames.test.ts b/web/src/components/lantern/AreaNames.test.ts index 88234cda5..ee781a2ce 100644 --- a/web/src/components/lantern/AreaNames.test.ts +++ b/web/src/components/lantern/AreaNames.test.ts @@ -62,13 +62,15 @@ function mountNames( * The words column as the layout draws it: the wrap, its scrolling story, and * in it one narrator's card or, with `line`, one row of the one column's * spoken lines (`.lq-cl > .lq-col > .lq-line`, the bubble at `card`), each at - * its own rectangle (top, bottom) across the stage. + * its own rectangle (top, bottom) across the stage; `head` is the speaker's + * name row (`.lq-line-name`) above the bubble. */ function cardsOver( wrap: [number, number], story: [number, number], card: [number, number], line = false, + head?: [number, number], ): void { const at = (el: HTMLElement, [top, bottom]: [number, number]): void => { vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({ @@ -94,6 +96,12 @@ function cardsOver( row.className = 'lq-cl lq-cl-left' const col = document.createElement('div') col.className = 'lq-col' + if (head) { + const name = document.createElement('b') + name.className = 'lq-line-name' + col.append(name) + at(name, head) + } col.append(cardEl) row.append(col) storyEl.append(row) @@ -498,6 +506,16 @@ describe('AreaNames', () => { expect(markAt(w, 'villager')).toEqual({ x: 144, y: 270 }) }) + it('counts the speaker name row above a spoken line as words, so no dot stands under it (rq-qz71)', async () => { + const w = mountNames({ showing: true }) + // The name row (30..66) over the bubble (70..240): the way north's mark (480, 54) is under the name row. + cardsOver([0, 320], [0, 320], [70, 240], true, [30, 66]) + await settle(w, 'words') + const north = markAt(w, 'exit-north')! + expect(north.x).toBe(480) + expect(north.y).toBeLessThan(30) + }) + it('shows a thing above the story where a card has scrolled out of it (rq-a6o6)', async () => { const w = mountNames({ showing: true }) // The story starts at 100; the card above it (20..180) is only seen from 100 down. The way north's mark is at y 54. diff --git a/web/src/components/lantern/AreaNames.vue b/web/src/components/lantern/AreaNames.vue index 45d347ce2..224c98860 100644 --- a/web/src/components/lantern/AreaNames.vue +++ b/web/src/components/lantern/AreaNames.vue @@ -105,8 +105,9 @@ const PANELS = '.lq-show-names' /** The words over the picture beside the narrator's cards, under which a thing's mark and name wait. */ const WORDS = '.lq-arrival-card, .lq-ln, .lq-more-below, .lq-narration-hint' -/** The narrator's cards and the one column's spoken lines, each seen only where its scrolling story shows it. */ -const CARDS = '.lq-story-wrap .lq-card, .lq-story-wrap .lq-line' +/** The narrator's cards and the one column's spoken lines with their speaker name rows, each seen only where its scrolling story shows it. */ +const CARDS = + '.lq-story-wrap .lq-card, .lq-story-wrap .lq-line, .lq-story-wrap .lq-line-name' const root = ref(null) const view = ref({ width: 0, height: 0 }) From 1e844077374967a5d3ac7c7974adeff7150632e2 Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Wed, 11 Nov 2026 20:12:35 +0100 Subject: [PATCH 2/3] fix(rq-qz71): Count a spoken line's whole column, and the two-sides speaker name, as words Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WCDKexRotFmmxHZ234zGww --- web/src/components/lantern/AreaNames.test.ts | 29 ++++++++++++++++++++ web/src/components/lantern/AreaNames.vue | 8 +++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/web/src/components/lantern/AreaNames.test.ts b/web/src/components/lantern/AreaNames.test.ts index ee781a2ce..1784fe02b 100644 --- a/web/src/components/lantern/AreaNames.test.ts +++ b/web/src/components/lantern/AreaNames.test.ts @@ -104,6 +104,8 @@ function cardsOver( } col.append(cardEl) row.append(col) + // The column holds the name row (if any) and the bubble. + at(col, [head ? head[0] : card[0], card[1]]) storyEl.append(row) } else storyEl.append(cardEl) wrapEl.append(storyEl) @@ -516,6 +518,33 @@ describe('AreaNames', () => { expect(north.y).toBeLessThan(30) }) + it('counts the speaker name above a two-sides spoken line as words (rq-qz71)', async () => { + const w = mountNames({ showing: true }) + const rect = (top: number, bottom: number): DOMRect => + ({ + left: 0, + top, + width: 960, + height: bottom - top, + right: 960, + bottom, + x: 0, + y: top, + toJSON: () => ({}), + }) as DOMRect + const line = document.createElement('div') + line.className = 'lq-ln' + const name = document.createElement('b') + name.className = 'lq-line-name' + line.append(name) + document.querySelector('.lq-stage')!.append(line) + vi.spyOn(line, 'getBoundingClientRect').mockReturnValue(rect(70, 240)) + // The name hangs above the line's own box, over the way north's mark (480, 54). + vi.spyOn(name, 'getBoundingClientRect').mockReturnValue(rect(30, 66)) + await settle(w, 'words') + expect(markAt(w, 'exit-north')!.y).toBeLessThan(30) + }) + it('shows a thing above the story where a card has scrolled out of it (rq-a6o6)', async () => { const w = mountNames({ showing: true }) // The story starts at 100; the card above it (20..180) is only seen from 100 down. The way north's mark is at y 54. diff --git a/web/src/components/lantern/AreaNames.vue b/web/src/components/lantern/AreaNames.vue index 224c98860..fb12aef44 100644 --- a/web/src/components/lantern/AreaNames.vue +++ b/web/src/components/lantern/AreaNames.vue @@ -104,10 +104,10 @@ const PANELS = '.lq-stage-plate, .lq-phone-plate, .lq-stage-buttons button, .lq-words-hide, .lq-paper-tab, ' + '.lq-show-names' /** The words over the picture beside the narrator's cards, under which a thing's mark and name wait. */ -const WORDS = '.lq-arrival-card, .lq-ln, .lq-more-below, .lq-narration-hint' -/** The narrator's cards and the one column's spoken lines with their speaker name rows, each seen only where its scrolling story shows it. */ -const CARDS = - '.lq-story-wrap .lq-card, .lq-story-wrap .lq-line, .lq-story-wrap .lq-line-name' +const WORDS = + '.lq-arrival-card, .lq-ln, .lq-ln .lq-line-name, .lq-more-below, .lq-narration-hint' +/** The narrator's cards and the one column's spoken lines (the whole `.lq-col`: speaker name row and bubble, both of which take a press), each seen only where its scrolling story shows it. */ +const CARDS = '.lq-story-wrap .lq-card, .lq-story-wrap .lq-col' const root = ref(null) const view = ref({ width: 0, height: 0 }) From 1a63a965262ebb0fd394393d96089c46b61b06db Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Wed, 11 Nov 2026 20:16:37 +0100 Subject: [PATCH 3/3] fix(rq-qz71): Count the whole spoken-line row, portrait included, as words Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WCDKexRotFmmxHZ234zGww --- web/src/components/lantern/AreaNames.test.ts | 20 ++++++++++++-------- web/src/components/lantern/AreaNames.vue | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/web/src/components/lantern/AreaNames.test.ts b/web/src/components/lantern/AreaNames.test.ts index 1784fe02b..67c593197 100644 --- a/web/src/components/lantern/AreaNames.test.ts +++ b/web/src/components/lantern/AreaNames.test.ts @@ -72,13 +72,17 @@ function cardsOver( line = false, head?: [number, number], ): void { - const at = (el: HTMLElement, [top, bottom]: [number, number]): void => { + const at = ( + el: HTMLElement, + [top, bottom]: [number, number], + right = 960, + ): void => { vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({ left: 0, top, - width: 960, + width: right, height: bottom - top, - right: 960, + right, bottom, x: 0, y: top, @@ -100,12 +104,12 @@ function cardsOver( const name = document.createElement('b') name.className = 'lq-line-name' col.append(name) - at(name, head) + at(name, head, 120) } col.append(cardEl) row.append(col) - // The column holds the name row (if any) and the bubble. - at(col, [head ? head[0] : card[0], card[1]]) + // The row holds the name row (if any) and the bubble; the name is only as wide as its words. + at(row, [head ? head[0] : card[0], card[1]]) storyEl.append(row) } else storyEl.append(cardEl) wrapEl.append(storyEl) @@ -508,9 +512,9 @@ describe('AreaNames', () => { expect(markAt(w, 'villager')).toEqual({ x: 144, y: 270 }) }) - it('counts the speaker name row above a spoken line as words, so no dot stands under it (rq-qz71)', async () => { + it('counts the whole row of a spoken line, speaker name row included, as words, so no dot stands under it (rq-qz71)', async () => { const w = mountNames({ showing: true }) - // The name row (30..66) over the bubble (70..240): the way north's mark (480, 54) is under the name row. + // A narrow name (x 0..120, y 30..66) over the bubble (70..240): the way north's mark (480, 54) is beside the name, above the bubble, still in the row. cardsOver([0, 320], [0, 320], [70, 240], true, [30, 66]) await settle(w, 'words') const north = markAt(w, 'exit-north')! diff --git a/web/src/components/lantern/AreaNames.vue b/web/src/components/lantern/AreaNames.vue index fb12aef44..e6870d572 100644 --- a/web/src/components/lantern/AreaNames.vue +++ b/web/src/components/lantern/AreaNames.vue @@ -106,8 +106,8 @@ const PANELS = /** The words over the picture beside the narrator's cards, under which a thing's mark and name wait. */ const WORDS = '.lq-arrival-card, .lq-ln, .lq-ln .lq-line-name, .lq-more-below, .lq-narration-hint' -/** The narrator's cards and the one column's spoken lines (the whole `.lq-col`: speaker name row and bubble, both of which take a press), each seen only where its scrolling story shows it. */ -const CARDS = '.lq-story-wrap .lq-card, .lq-story-wrap .lq-col' +/** The narrator's cards and the one column's spoken lines (the whole `.lq-cl` row: the speaker's portrait, name row and bubble, all of which take a press), each seen only where its scrolling story shows it. */ +const CARDS = '.lq-story-wrap .lq-card, .lq-story-wrap .lq-cl' const root = ref(null) const view = ref({ width: 0, height: 0 })