Extract title tags & per-row chevron expand

Add parsing and UX for bracketed title metadata and per-row title expansion. Introduces extractTitleTags (src/lib/utils/title-tags.ts) to pull bracketed tags from result titles (de-duplicated, slash-split) and useIsTruncated (src/lib/hooks/useIsTruncated.ts) to detect horizontal overflow. Refactors InteractiveTorrentSearchModal to a ResultRow component that renders title chips (slate chips) for parsed tags (filtered vs displayFormat), shows a chevron disclosure only when the title is truncated (or while expanded), toggles expansion per GUID, and resets expansion state when the modal closes. Tests added/updated for the component, hook, and parser; documentation updated to reflect behavior.
This commit is contained in:
kikootwo
2026-05-16 10:41:44 -04:00
parent 31d30bdfa0
commit 1065577a04
7 changed files with 619 additions and 120 deletions
+45
View File
@@ -0,0 +1,45 @@
/**
* Component: useIsTruncated Hook Tests
* Documentation: documentation/frontend/components.md
*/
// @vitest-environment jsdom
import React, { useRef } from 'react';
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useIsTruncated } from '@/lib/hooks/useIsTruncated';
function Probe({ scrollWidth, clientWidth }: { scrollWidth: number; clientWidth: number }) {
const ref = useRef<HTMLSpanElement | null>(null);
const attach = (el: HTMLSpanElement | null) => {
ref.current = el;
if (el) {
Object.defineProperty(el, 'scrollWidth', { configurable: true, value: scrollWidth });
Object.defineProperty(el, 'clientWidth', { configurable: true, value: clientWidth });
}
};
const truncated = useIsTruncated(ref);
return (
<span ref={attach} data-testid="probe" data-truncated={truncated ? 'yes' : 'no'}>
probe
</span>
);
}
describe('useIsTruncated', () => {
it('returns false when scrollWidth fits inside clientWidth', () => {
const { getByTestId } = render(<Probe scrollWidth={80} clientWidth={120} />);
expect(getByTestId('probe').getAttribute('data-truncated')).toBe('no');
});
it('returns false when scrollWidth equals clientWidth', () => {
const { getByTestId } = render(<Probe scrollWidth={100} clientWidth={100} />);
expect(getByTestId('probe').getAttribute('data-truncated')).toBe('no');
});
it('returns true when scrollWidth exceeds clientWidth', () => {
const { getByTestId } = render(<Probe scrollWidth={400} clientWidth={120} />);
expect(getByTestId('probe').getAttribute('data-truncated')).toBe('yes');
});
});
+112
View File
@@ -0,0 +1,112 @@
/**
* Tests for extractTitleTags — one case per row of the Edge Cases table
* in .zach-flow/engineering-brief.md.
*/
import { describe, it, expect } from 'vitest';
import { extractTitleTags } from '@/lib/utils/title-tags';
describe('extractTitleTags', () => {
it('returns the original title and no tags when there are no brackets', () => {
expect(extractTitleTags('Foundation')).toEqual({ residual: 'Foundation', tags: [] });
});
it('extracts a single bracketed tag', () => {
expect(extractTitleTags('Foundation [German]')).toEqual({
residual: 'Foundation',
tags: ['German'],
});
});
it('splits a single bracket group on slash', () => {
expect(extractTitleTags('Foundation [German / Unabridged]')).toEqual({
residual: 'Foundation',
tags: ['German', 'Unabridged'],
});
});
it('extracts multiple bracket groups in order', () => {
expect(extractTitleTags('Foundation [German] [Unabridged]')).toEqual({
residual: 'Foundation',
tags: ['German', 'Unabridged'],
});
});
it('collapses inner whitespace and trailing text after stripped brackets', () => {
expect(extractTitleTags('Foundation [German] [Unabridged] v2')).toEqual({
residual: 'Foundation v2',
tags: ['German', 'Unabridged'],
});
});
it('handles a leading bracket group', () => {
expect(extractTitleTags('[Audible] Foundation')).toEqual({
residual: 'Foundation',
tags: ['Audible'],
});
});
it('leaves a malformed unclosed bracket in the residual and returns no tags', () => {
expect(extractTitleTags('Foundation [unclosed')).toEqual({
residual: 'Foundation [unclosed',
tags: [],
});
});
it('treats an empty bracket group as no tags', () => {
expect(extractTitleTags('Foundation []')).toEqual({
residual: 'Foundation',
tags: [],
});
});
it('treats a bracket group of only separators as no tags', () => {
expect(extractTitleTags('Foundation [ / / ]')).toEqual({
residual: 'Foundation',
tags: [],
});
});
it('splits a bracket group with multiple slash-separated values', () => {
expect(extractTitleTags('Foundation [a/b/c]')).toEqual({
residual: 'Foundation',
tags: ['a', 'b', 'c'],
});
});
it('extracts the inner tag from a nested bracket group and accepts the partial residual', () => {
expect(extractTitleTags('Foundation [Edition [Deluxe]]')).toEqual({
residual: 'Foundation [Edition ]',
tags: ['Deluxe'],
});
});
it('de-duplicates tags case-insensitively, preserving first-seen casing', () => {
expect(extractTitleTags('Foundation [German] [german]')).toEqual({
residual: 'Foundation',
tags: ['German'],
});
});
it('trims surrounding whitespace from the title', () => {
expect(extractTitleTags(' Foundation [German] ')).toEqual({
residual: 'Foundation',
tags: ['German'],
});
});
it('handles 200+ char titles', () => {
const longBody = 'A'.repeat(220);
const result = extractTitleTags(`${longBody} [German]`);
expect(result.tags).toEqual(['German']);
expect(result.residual).toBe(longBody);
});
it('returns empty values for an empty string', () => {
expect(extractTitleTags('')).toEqual({ residual: '', tags: [] });
});
it('returns empty values for a whitespace-only string', () => {
expect(extractTitleTags(' ')).toEqual({ residual: '', tags: [] });
});
});