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} + /> + + ); +}