Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cb64c96
Add read-only diff components for activity before/after previews
myieye Oct 26, 2026
9166db5
Show diffs in hidden writing systems and harden diff leaves
myieye Oct 26, 2026
7762609
Show custom view writing-system resets and picture reorders in diffs
myieye Oct 26, 2026
2b30ee9
Only read a missing writing-system list as "all" when the view exists
myieye Oct 26, 2026
105953d
Mark reordered selections in multi-select diffs
myieye Oct 26, 2026
532f931
Guard editor fields in entityConfig and diff morph type under the lex…
myieye Oct 27, 2026
d56bebe
PR screenshots (removed in the next commit)
Oct 27, 2026
4c5a669
Remove PR screenshots
Oct 27, 2026
ab9c8e7
PR screenshots (removed in the next commit)
Oct 27, 2026
db9f3da
Remove PR screenshots
Oct 27, 2026
1dbbecc
Show changed fields the view hides in entry, sense and example diffs
claude Oct 27, 2026
5807aa9
Compare hidden list fields the way their diffs render them
claude Oct 27, 2026
0dedfb1
PR screenshots (removed in the next commit)
Oct 27, 2026
11b25a8
Remove PR screenshots
Oct 27, 2026
fbbcb64
PR screenshots (removed in the next commit)
Oct 27, 2026
dd9aa9c
Remove PR screenshots
Oct 27, 2026
4f8685f
Lay out diff writing-system rows like the editor's
claude Oct 28, 2026
75d5f96
PR screenshots (removed in the next commit)
Oct 28, 2026
f3dfbca
Remove PR screenshots
Oct 28, 2026
93cdf6a
Show homograph numbers on headwords in the FW Lite viewer
claude Oct 31, 2026
4d2ddfe
Merge claude/activity-3-diff-view into claude/homograph-subscripts
claude Oct 31, 2026
4481a64
screenshot: pr-2881-homograph-subscripts/7108fce12016ccb8.png
Oct 31, 2026
1edf6ba
remove screenshot: pr-2881-homograph-subscripts/7108fce12016ccb8.png
Oct 31, 2026
3ce7f90
screenshot: pr-2881-homograph-subscripts/5194f423354f48b8.png
Oct 31, 2026
a6da721
remove screenshot: pr-2881-homograph-subscripts/5194f423354f48b8.png
Oct 31, 2026
599721d
screenshot: pr-2881-homograph-subscripts/c88bb7cb7c7bbf39.png
Oct 31, 2026
31b11a3
remove screenshot: pr-2881-homograph-subscripts/c88bb7cb7c7bbf39.png
Oct 31, 2026
507a13a
Size homograph subscripts at 0.6em
claude Oct 31, 2026
3f71563
screenshot: pr-2881-homograph-subscripts/85b131618711af31.png
Oct 31, 2026
151e3d5
remove screenshot: pr-2881-homograph-subscripts/85b131618711af31.png
Oct 31, 2026
2959eed
screenshot: pr-2881-homograph-subscripts/a42e138e0d92cc93.png
Oct 31, 2026
f70fbcc
remove screenshot: pr-2881-homograph-subscripts/a42e138e0d92cc93.png
Oct 31, 2026
2c4d904
screenshot: pr-2881-homograph-subscripts/63f5eafdd1e4bdaa.png
Oct 31, 2026
9d5e8af
remove screenshot: pr-2881-homograph-subscripts/63f5eafdd1e4bdaa.png
Oct 31, 2026
3200e47
Merge develop after #2814's squash merge
claude Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type {HTMLAttributes} from 'svelte/elements';
import {useWritingSystemService} from '$project/data';
import {useViewService} from '$lib/views/view-service.svelte';
import HomographNumber from './HomographNumber.svelte';

let {
entry,
Expand Down Expand Up @@ -45,7 +46,7 @@
{#each headwords as headword, i (headword.wsId)}
<!-- eslint-disable-next-line svelte/no-useless-mustaches This mustache is not useless, it preserves whitespace -->
{#if i > 0}{' / '}{/if}
<span class={headword.color}>{headword.value}</span>
<span class={headword.color}>{headword.value}<HomographNumber homographNumber={entry.homographNumber} /></span>
{:else}
{#if placeholder}
<span class="text-muted-foreground">{placeholder}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts" module>
const subscriptDigits = '₀₁₂₃₄₅₆₇₈₉';

/**
* Plain-text form of {@link HomographNumber}, for places that only take a string (e.g. a `title`).
* Display only: never append it to headwords used for search, sorting or matching.
*/
export function homographSubscript(homographNumber: number): string {
if (!(homographNumber > 0)) return '';
return String(homographNumber).replace(/\d/g, (digit) => subscriptDigits[Number(digit)]);
}
</script>

<script lang="ts">
import type {HTMLAttributes} from 'svelte/elements';
import {cn} from '$lib/utils';

let {
homographNumber,
class: className,
...restProps
}: HTMLAttributes<HTMLElement> & {
/** FLEx-style: 0 means the entry has no homographs, so nothing is shown */
homographNumber: number;
} = $props();
</script>

<!-- Rendered directly after a headword, like FLEx: bank₁, bank₂. 0.6em matches the Unicode subscript digits; the preflight 75% is too big. -->
{#if homographNumber > 0}<sub data-testid="homograph-number" class={cn('text-[0.6em]', className)} {...restProps}
>{homographNumber}</sub
>{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import {DEFAULT_DEBOUNCE_TIME} from '$lib/utils/time';
import {cn} from '$lib/utils';
import {useWritingSystemService} from '$project/data';
import HomographNumber from '$lib/components/dictionary/HomographNumber.svelte';

const viewService = useViewService();
const writingSystemService = useWritingSystemService();
Expand Down Expand Up @@ -220,7 +221,7 @@
<span class="flex items-center gap-1.5 truncate">
<span>{pt($t`Entry or sense:`, $t`Word or meaning:`, viewService.currentView)}</span>
<span class="text-foreground truncate">
{collapsedSelectionPreview}
{collapsedSelectionPreview}{#if !selectedSense && collapsedSelectionPreview}<HomographNumber homographNumber={selectedEntry.homographNumber} />{/if}
</span>
</span>
<Icon icon={isEntryOrSenseOpen ? 'i-mdi-chevron-down' : 'i-mdi-chevron-up'} class="size-5"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import {Icon} from '$lib/components/ui/icon';
import {Button} from '$lib/components/ui/button';
import DictionaryEntry from '$lib/components/dictionary/DictionaryEntry.svelte';
import {homographSubscript} from '$lib/components/dictionary/HomographNumber.svelte';
import Loading from '$lib/components/Loading.svelte';
import {useMiniLcmApi} from '$lib/services/service-provider';
import {useWritingSystemService} from '$project/data';
Expand Down Expand Up @@ -127,7 +128,13 @@
});
const hasExactWordMatch = $derived(!!matches?.some((match) => match.kind === 'same-word'));
const previewHeadwords = $derived(
[...new Set((matches ?? []).map((match) => writingSystemService.firstHeadword(match.entry)).filter(Boolean))].join(', '),
// Display only; the subscript keeps homographs apart instead of collapsing them into one name
[...new Set((matches ?? [])
.map((match) => {
const headword = writingSystemService.firstHeadword(match.entry);
return headword && headword + homographSubscript(match.entry.homographNumber);
})
.filter(Boolean))].join(', '),
);
const summaryMessage = $derived.by(() => {
if (hasExactWordMatch)
Expand Down
5 changes: 4 additions & 1 deletion frontend/viewer/src/project/browse/EntryMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {useAppLauncherService} from '$lib/services/app-launcher-service';
import {IsMobile} from '$lib/hooks/is-mobile.svelte';
import OpenInFieldWorksButton from '$lib/components/OpenInFieldWorksButton.svelte';
import {homographSubscript} from '$lib/components/dictionary/HomographNumber.svelte';

const multiWindowService = useMultiWindowService();
const dialogsService = useDialogsService();
Expand All @@ -29,7 +30,9 @@
children?: Snippet
}>();

const headword = $derived((entry && writingSystemService.viewBestHeadword(entry, viewService.currentView)) || $t`Untitled`);
const bestHeadword = $derived(entry ? writingSystemService.viewBestHeadword(entry, viewService.currentView) : '');
// Display only (the delete prompt): the subscript tells homographs apart
const headword = $derived(entry && bestHeadword ? bestHeadword + homographSubscript(entry.homographNumber) : $t`Untitled`);

let open = $state(false);

Expand Down
3 changes: 2 additions & 1 deletion frontend/viewer/src/project/browse/EntryRow.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import DictionaryEntry from '$lib/components/dictionary/DictionaryEntry.svelte';
import HomographNumber from '$lib/components/dictionary/HomographNumber.svelte';
import ListItem, {type ListItemProps} from '$lib/components/ListItem.svelte';
import Badge from '$lib/components/ui/badge/badge.svelte';
import type {IEntry} from '$lib/dotnet-types';
Expand Down Expand Up @@ -70,7 +71,7 @@
{#if fallbackWs}
<span class="-mr-1 relative -bottom-px text-xs font-normal text-foreground/80" title={fallbackWs.name}>{fallbackWs.abbreviation || fallbackWs.wsId}</span>
{/if}
{best?.value || $t`Untitled`}
{#if best?.value}{best.value}<HomographNumber homographNumber={entry.homographNumber} />{:else}{$t`Untitled`}{/if}
</span>
{@render badge?.()}
</h2>
Expand Down
9 changes: 7 additions & 2 deletions frontend/viewer/src/project/browse/EntryView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import {useFeatures} from '$lib/services/feature-service';
import type {ReadonlyDeep} from 'type-fest';
import DictionaryEntry from '$lib/components/dictionary/DictionaryEntry.svelte';
import HomographNumber, {homographSubscript} from '$lib/components/dictionary/HomographNumber.svelte';
import * as Alert from '$lib/components/ui/alert';
import {pt} from '$lib/views/view-text';
import {useViewService} from '$lib/views/view-service.svelte';
Expand Down Expand Up @@ -109,7 +110,9 @@
let latestPersistedSnapshot = $state<ReadonlyDeep<IEntry>>();
const entryPersistence = new EntryPersistence(() => latestPersistedSnapshot);
let entry = $derived(entryResource.current ?? undefined);
const headword = $derived((entry && writingSystemService.viewBestHeadword(entry, viewService.currentView)) || $t`Untitled`);
const bestHeadword = $derived(entry ? writingSystemService.viewBestHeadword(entry, viewService.currentView) : '');
// Plain text for the title tooltip; the heading itself renders the homograph number as a <sub>
const headwordTitle = $derived(entry && bestHeadword ? bestHeadword + homographSubscript(entry.homographNumber) : $t`Untitled`);
const loadingDebounced = new Debounced(() => entryResource.loading, 50);
const dictionaryPreview: DictionaryPreviewMode = $derived(
isDictionaryPreviewMode(dictionaryPreviewStorage.current) ? dictionaryPreviewStorage.current : 'show'
Expand Down Expand Up @@ -183,7 +186,9 @@
<!-- flex-1 truncates a long unbreakable headword so it can't push the actions off screen. -->
<h2
class="ml-4 text-2xl font-semibold mb-2 min-w-0 flex-1 truncate max-md:text-center"
title={headword}>{headword}</h2>
title={headwordTitle}>
{#if bestHeadword}{bestHeadword}<HomographNumber homographNumber={entry.homographNumber} />{:else}{headwordTitle}{/if}
</h2>
<div class="flex shrink-0">
{#if features.comments}
<div class="relative">
Expand Down
3 changes: 2 additions & 1 deletion frontend/viewer/src/project/tasks/ReviewList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {useWritingSystemService} from '$project/data';
import {t} from 'svelte-i18n-lingui';
import {useViewService} from '$lib/views/view-service.svelte';
import HomographNumber from '$lib/components/dictionary/HomographNumber.svelte';

const writingSystemService = useWritingSystemService();
const viewService = useViewService();
Expand All @@ -27,7 +28,7 @@
<div class="flex flex-col">
{#each subjects as subject (subject)}
<ListItem class="my-2" onclick={() => editSubject(subject)} icon="i-mdi-book-open-page-variant">
<p>{writingSystemService.viewBestHeadword(subject.entry, viewService.currentView)}</p>
<p>{writingSystemService.viewBestHeadword(subject.entry, viewService.currentView)}<HomographNumber homographNumber={subject.entry.homographNumber} /></p>
<p>{subject.subject}</p>
</ListItem>
{/each}
Expand Down
66 changes: 66 additions & 0 deletions frontend/viewer/tests/ui/homograph-numbers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {expect, test} from '@playwright/test';

import {DemoProjectPage} from './demo-project.page';
import {MorphTypeKind} from '$lib/dotnet-types/generated-types/MiniLcm/Models/MorphTypeKind';

// FLEx shows homograph number N > 0 as a subscript after the headword (bank₁, bank₂); 0 shows nothing.
test.describe('Homograph numbers', () => {
let projectPage: DemoProjectPage;
const headword = 'qhomograph';

test.beforeEach(async ({page}) => {
projectPage = new DemoProjectPage(page);
await projectPage.goto();
await page.evaluate(async ({hw, morphType}) => {
const api = window.__PLAYWRIGHT_UTILS__.demoApi;
for (const homographNumber of [1, 2, 0]) {
await api.createEntry({
id: crypto.randomUUID(),
// the 0 entry gets a different headword, so it can be told apart
lexemeForm: {seh: homographNumber ? hw : hw + 'solo'},
citationForm: {},
senses: [],
note: {},
literalMeaning: {},
morphType,
components: [],
complexForms: [],
complexFormTypes: [],
publishIn: [],
homographNumber,
}, {includeComplexFormsAndComponents: true, autoAddMainPublication: false});
}
}, {hw: headword, morphType: MorphTypeKind.Stem});
// Only part of the headword, so the "create from search" row can't match it.
await projectPage.entriesList.filterByText('qhomogr');
});

function entryRows() {
return projectPage.entriesList.entryRows.filter({hasNotText: 'Add to dictionary'});
}

function rowWithHomographNumber(n: string) {
return entryRows().filter({has: projectPage.page.getByTestId('homograph-number').filter({hasText: new RegExp(`^${n}$`)})});
}

test('entry list shows the homograph number as a subscript, and nothing for 0', async () => {
const rows = entryRows();
await expect(rows).toHaveCount(3);
await expect(rows.filter({hasText: 'qhomographsolo'}).getByTestId('homograph-number')).toHaveCount(0);
await expect(rowWithHomographNumber('1')).toHaveCount(1);
await expect(rowWithHomographNumber('2')).toHaveCount(1);
});

test('entry editor title and dictionary preview show the homograph number', async ({page}) => {
await rowWithHomographNumber('2').click();
await projectPage.entryView.waitForEntryLoaded();

// The plain-text title uses Unicode subscript digits; the visible heading uses <sub>
const title = page.locator(`h2[title="${headword}₂"]`);
await expect(title).toBeVisible();
await expect(title.getByTestId('homograph-number')).toHaveText('2');

const preview = page.locator('strong', {hasText: headword});
await expect(preview.getByTestId('homograph-number')).toHaveText('2');
});
});
Loading