From e6930e00ac00cb70cdd9c59c081707eae8ccdf7a Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 15:01:53 +0200 Subject: [PATCH] refactor(studio): share the typeface buttons and the weight line between the inspectors The text inspector and the travel inspector each drew the same row of typeface buttons and the same weight choice. Both now render FontButtons and WeightLine from StudioTypeControls.tsx. The travel inspector keeps casting its patches to Partial, so each side writes exactly the fields it wrote before. --- .../src/components/Studio/StudioInspector.tsx | 38 +-------- .../Studio/StudioTravelInspector.tsx | 43 ++-------- .../Studio/StudioTypeControls.test.tsx | 50 ++++++++++++ .../components/Studio/StudioTypeControls.tsx | 81 +++++++++++++++++++ 4 files changed, 142 insertions(+), 70 deletions(-) create mode 100644 client/src/components/Studio/StudioTypeControls.test.tsx create mode 100644 client/src/components/Studio/StudioTypeControls.tsx diff --git a/client/src/components/Studio/StudioInspector.tsx b/client/src/components/Studio/StudioInspector.tsx index 7e9a163ec..7d27d96ad 100644 --- a/client/src/components/Studio/StudioInspector.tsx +++ b/client/src/components/Studio/StudioInspector.tsx @@ -6,7 +6,6 @@ import { import { useMemo, useRef, useState } from 'react' import { useStudioStore } from '../../store/studioStore' import { photoSrc } from './bookRender' -import { BOOK_FONTS, BOOK_FONT_ORDER, hasWeight, nearestWeight } from './bookFonts' import { FRAME_SHAPES, SHAPE_GROUPS } from './shapes' import { ShapeGlyph } from './StudioElementsPanel' import { TravelInspector } from './StudioTravelInspector' @@ -14,6 +13,7 @@ import { Swatches } from './StudioSwatches' import { formatBookCoords } from './entryText' import { iconComponent, iconLabel, searchIcons } from './iconLibrary' import { Choice, Line, NumField, Section, Switch } from './StudioControls' +import { FontButtons, WeightLine } from './StudioTypeControls' import type { JourneySource } from './StudioSidebar' import { Tooltip } from '../shared/Tooltip' @@ -320,41 +320,9 @@ export function StudioInspector({ nothing about a typeface — and grouped, because "which serif" is a different question from "serif or sans". */} -
- {BOOK_FONT_ORDER.map(id => { - const font = BOOK_FONTS[id] - return ( - - ) - })} -
+ set(patch)} />
- - ({ - value: w, - label: String(w), - disabled: !hasWeight(el.font, w), - title: hasWeight(el.font, w) ? undefined : t('journey.studio.weightMissing'), - }))} - onPick={weight => set({ weight })} - /> - + set({ weight })} t={t} />
{([['left', AlignLeft], ['center', AlignCenter], ['right', AlignRight], ['justify', AlignJustify]] as const).map(([a, Icon]) => ( diff --git a/client/src/components/Studio/StudioTravelInspector.tsx b/client/src/components/Studio/StudioTravelInspector.tsx index 8754ed238..50908e230 100644 --- a/client/src/components/Studio/StudioTravelInspector.tsx +++ b/client/src/components/Studio/StudioTravelInspector.tsx @@ -6,7 +6,7 @@ import type { import { BOOK_METRICS } from '@trek/shared' import { isStale, refreshPatch } from './travelRefresh' import { Swatches } from './StudioSwatches' -import { BOOK_FONTS, BOOK_FONT_ORDER, hasWeight, nearestWeight } from './bookFonts' +import { FontButtons, WeightLine } from './StudioTypeControls' import { sourceIdOf, useMapSources } from './mapSources' import { printDpi, tileView } from './mapTiles' import { useCartoApiKey } from '../../hooks/useTileUrl' @@ -82,41 +82,14 @@ export function TravelInspector({ el, stats, set, t }: TravelInspectorProps) { * wrong typeface and no way to fix it. */}
-
- {BOOK_FONT_ORDER.map(id => { - const font = BOOK_FONTS[id] - return ( - - ) - })} -
+ set(patch as Partial)} />
- - ({ - value: w, - label: String(w), - disabled: !hasWeight(el.font, w), - title: hasWeight(el.font, w) ? undefined : t('journey.studio.weightMissing'), - }))} - onPick={weight => set({ weight } as Partial)} - /> - + set({ weight } as Partial)} + t={t} + /> k; + +describe('FontButtons', () => { + it('offers every bundled family in the picker order and marks the current one', () => { + render(); + const buttons = screen.getAllByRole('button'); + expect(buttons).toHaveLength(BOOK_FONT_ORDER.length); + expect(screen.getByRole('button', { name: 'Lora' }).className).toContain('is-on'); + expect(screen.getByRole('button', { name: 'Poppins' }).className).not.toContain('is-on'); + }); + + it('keeps the weight when the new family ships it', () => { + const onPick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: 'Inter' })); + expect(onPick).toHaveBeenCalledWith({ font: 'inter', weight: 700 }); + }); + + it('moves the weight to one the new family really has', () => { + const onPick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: 'Bebas Neue' })); + expect(onPick).toHaveBeenCalledWith({ font: 'bebas', weight: 400 }); + }); +}); + +describe('WeightLine', () => { + it('greys the weights the family does not ship and says why', () => { + render(); + expect(screen.getByText('journey.studio.weight')).toBeTruthy(); + const bold = screen.getByRole('radio', { name: '700' }) as HTMLButtonElement; + expect(bold.disabled).toBe(true); + expect(bold.title).toBe('journey.studio.weightMissing'); + const regular = screen.getByRole('radio', { name: '400' }) as HTMLButtonElement; + expect(regular.disabled).toBe(false); + expect(regular.getAttribute('aria-checked')).toBe('true'); + }); + + it('hands the picked weight back', () => { + const onPick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('radio', { name: '600' })); + expect(onPick).toHaveBeenCalledWith(600); + }); +}); diff --git a/client/src/components/Studio/StudioTypeControls.tsx b/client/src/components/Studio/StudioTypeControls.tsx new file mode 100644 index 000000000..c46853b17 --- /dev/null +++ b/client/src/components/Studio/StudioTypeControls.tsx @@ -0,0 +1,81 @@ +import type { BookTextElement } from '@trek/shared'; +import type { BookFontId } from './bookFonts'; +import { BOOK_FONTS, BOOK_FONT_ORDER, hasWeight, nearestWeight } from './bookFonts'; +import { Choice, Line } from './StudioControls'; + +/** + * The typeface buttons and the weight line, shared by the text inspector and + * the travel inspector so both set type the same way. + */ + +/** The weight set the book schema allows, taken from it so there is one list. */ +type BookWeight = BookTextElement['weight']; + +const WEIGHTS = [400, 500, 600, 700] as const; + +/** One button per bundled family, each set in its own face. */ +export function FontButtons({ + font, + weight, + onPick, +}: { + font: string; + weight: BookWeight; + onPick: (patch: { font: BookFontId; weight: BookWeight }) => void; +}) { + return ( +
+ {BOOK_FONT_ORDER.map((id) => { + const face = BOOK_FONTS[id]; + return ( + + ); + })} +
+ ); +} + +/** The four weights, with the ones the family does not ship greyed out. */ +export function WeightLine({ + font, + weight, + onPick, + t, +}: { + font: string; + weight: BookWeight; + onPick: (weight: BookWeight) => void; + t: (k: string) => string; +}) { + return ( + + ({ + value: w, + label: String(w), + disabled: !hasWeight(font, w), + title: hasWeight(font, w) ? undefined : t('journey.studio.weightMissing'), + }))} + onPick={onPick} + /> + + ); +}