mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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<BookElement>, so each side writes exactly the fields it wrote before.
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
|||||||
import { useMemo, useRef, useState } from 'react'
|
import { useMemo, useRef, useState } from 'react'
|
||||||
import { useStudioStore } from '../../store/studioStore'
|
import { useStudioStore } from '../../store/studioStore'
|
||||||
import { photoSrc } from './bookRender'
|
import { photoSrc } from './bookRender'
|
||||||
import { BOOK_FONTS, BOOK_FONT_ORDER, hasWeight, nearestWeight } from './bookFonts'
|
|
||||||
import { FRAME_SHAPES, SHAPE_GROUPS } from './shapes'
|
import { FRAME_SHAPES, SHAPE_GROUPS } from './shapes'
|
||||||
import { ShapeGlyph } from './StudioElementsPanel'
|
import { ShapeGlyph } from './StudioElementsPanel'
|
||||||
import { TravelInspector } from './StudioTravelInspector'
|
import { TravelInspector } from './StudioTravelInspector'
|
||||||
@@ -14,6 +13,7 @@ import { Swatches } from './StudioSwatches'
|
|||||||
import { formatBookCoords } from './entryText'
|
import { formatBookCoords } from './entryText'
|
||||||
import { iconComponent, iconLabel, searchIcons } from './iconLibrary'
|
import { iconComponent, iconLabel, searchIcons } from './iconLibrary'
|
||||||
import { Choice, Line, NumField, Section, Switch } from './StudioControls'
|
import { Choice, Line, NumField, Section, Switch } from './StudioControls'
|
||||||
|
import { FontButtons, WeightLine } from './StudioTypeControls'
|
||||||
import type { JourneySource } from './StudioSidebar'
|
import type { JourneySource } from './StudioSidebar'
|
||||||
import { Tooltip } from '../shared/Tooltip'
|
import { Tooltip } from '../shared/Tooltip'
|
||||||
|
|
||||||
@@ -320,41 +320,9 @@ export function StudioInspector({
|
|||||||
nothing about a typeface — and grouped, because "which serif"
|
nothing about a typeface — and grouped, because "which serif"
|
||||||
is a different question from "serif or sans".
|
is a different question from "serif or sans".
|
||||||
*/}
|
*/}
|
||||||
<div className="st-fonts">
|
<FontButtons font={el.font} weight={el.weight} onPick={patch => set(patch)} />
|
||||||
{BOOK_FONT_ORDER.map(id => {
|
|
||||||
const font = BOOK_FONTS[id]
|
|
||||||
return (
|
|
||||||
<button type="button"
|
|
||||||
key={id}
|
|
||||||
className={`st-font ${el.font === id ? 'is-on' : ''}`}
|
|
||||||
style={{ fontFamily: font.stack }}
|
|
||||||
onClick={() => set({
|
|
||||||
font: id,
|
|
||||||
// A family that does not ship this weight would render a
|
|
||||||
// synthesised bold — a smeared regular in print — so the
|
|
||||||
// weight moves to the nearest one it really has.
|
|
||||||
weight: nearestWeight(id, el.weight) as typeof el.weight,
|
|
||||||
})}
|
|
||||||
title={font.name}
|
|
||||||
>
|
|
||||||
{font.name}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Line label={t('journey.studio.weight')}>
|
<WeightLine font={el.font} weight={el.weight} onPick={weight => set({ weight })} t={t} />
|
||||||
<Choice
|
|
||||||
value={el.weight}
|
|
||||||
options={([400, 500, 600, 700] as const).map(w => ({
|
|
||||||
value: w,
|
|
||||||
label: String(w),
|
|
||||||
disabled: !hasWeight(el.font, w),
|
|
||||||
title: hasWeight(el.font, w) ? undefined : t('journey.studio.weightMissing'),
|
|
||||||
}))}
|
|
||||||
onPick={weight => set({ weight })}
|
|
||||||
/>
|
|
||||||
</Line>
|
|
||||||
<Line label={t('journey.studio.align')}>
|
<Line label={t('journey.studio.align')}>
|
||||||
<div className="st-row">
|
<div className="st-row">
|
||||||
{([['left', AlignLeft], ['center', AlignCenter], ['right', AlignRight], ['justify', AlignJustify]] as const).map(([a, Icon]) => (
|
{([['left', AlignLeft], ['center', AlignCenter], ['right', AlignRight], ['justify', AlignJustify]] as const).map(([a, Icon]) => (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
import { BOOK_METRICS } from '@trek/shared'
|
import { BOOK_METRICS } from '@trek/shared'
|
||||||
import { isStale, refreshPatch } from './travelRefresh'
|
import { isStale, refreshPatch } from './travelRefresh'
|
||||||
import { Swatches } from './StudioSwatches'
|
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 { sourceIdOf, useMapSources } from './mapSources'
|
||||||
import { printDpi, tileView } from './mapTiles'
|
import { printDpi, tileView } from './mapTiles'
|
||||||
import { useCartoApiKey } from '../../hooks/useTileUrl'
|
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.
|
* wrong typeface and no way to fix it.
|
||||||
*/}
|
*/}
|
||||||
<Section label={t('journey.studio.typography')} defaultOpen={false}>
|
<Section label={t('journey.studio.typography')} defaultOpen={false}>
|
||||||
<div className="st-fonts">
|
<FontButtons font={el.font} weight={el.weight} onPick={patch => set(patch as Partial<BookElement>)} />
|
||||||
{BOOK_FONT_ORDER.map(id => {
|
|
||||||
const font = BOOK_FONTS[id]
|
|
||||||
return (
|
|
||||||
<button type="button"
|
|
||||||
key={id}
|
|
||||||
className={`st-font ${el.font === id ? 'is-on' : ''}`}
|
|
||||||
style={{ fontFamily: font.stack }}
|
|
||||||
onClick={() => set({
|
|
||||||
font: id,
|
|
||||||
// A family that does not ship this weight renders a
|
|
||||||
// synthesised bold — a smeared regular in print — so the
|
|
||||||
// weight moves to the nearest one it really has.
|
|
||||||
weight: nearestWeight(id, el.weight) as typeof el.weight,
|
|
||||||
} as Partial<BookElement>)}
|
|
||||||
title={font.name}
|
|
||||||
>
|
|
||||||
{font.name}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Line label={t('journey.studio.weight')}>
|
<WeightLine
|
||||||
<Choice
|
font={el.font}
|
||||||
value={el.weight}
|
weight={el.weight}
|
||||||
options={([400, 500, 600, 700] as const).map(w => ({
|
onPick={weight => set({ weight } as Partial<BookElement>)}
|
||||||
value: w,
|
t={t}
|
||||||
label: String(w),
|
/>
|
||||||
disabled: !hasWeight(el.font, w),
|
|
||||||
title: hasWeight(el.font, w) ? undefined : t('journey.studio.weightMissing'),
|
|
||||||
}))}
|
|
||||||
onPick={weight => set({ weight } as Partial<BookElement>)}
|
|
||||||
/>
|
|
||||||
</Line>
|
|
||||||
<Line label={t('journey.studio.textScale')}>
|
<Line label={t('journey.studio.textScale')}>
|
||||||
<Choice
|
<Choice
|
||||||
value={el.textScale}
|
value={el.textScale}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { fireEvent, render, screen } from '../../../tests/helpers/render';
|
||||||
|
import { BOOK_FONT_ORDER } from './bookFonts';
|
||||||
|
import { FontButtons, WeightLine } from './StudioTypeControls';
|
||||||
|
|
||||||
|
const t = (k: string) => k;
|
||||||
|
|
||||||
|
describe('FontButtons', () => {
|
||||||
|
it('offers every bundled family in the picker order and marks the current one', () => {
|
||||||
|
render(<FontButtons font="serif" weight={400} onPick={vi.fn()} />);
|
||||||
|
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(<FontButtons font="sans" weight={700} onPick={onPick} />);
|
||||||
|
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(<FontButtons font="sans" weight={700} onPick={onPick} />);
|
||||||
|
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(<WeightLine font="bebas" weight={400} onPick={vi.fn()} t={t} />);
|
||||||
|
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(<WeightLine font="sans" weight={400} onPick={onPick} t={t} />);
|
||||||
|
fireEvent.click(screen.getByRole('radio', { name: '600' }));
|
||||||
|
expect(onPick).toHaveBeenCalledWith(600);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<div className="st-fonts">
|
||||||
|
{BOOK_FONT_ORDER.map((id) => {
|
||||||
|
const face = BOOK_FONTS[id];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={id}
|
||||||
|
className={`st-font ${font === id ? 'is-on' : ''}`}
|
||||||
|
style={{ fontFamily: face.stack }}
|
||||||
|
onClick={() =>
|
||||||
|
onPick({
|
||||||
|
font: id,
|
||||||
|
// A family that does not ship this weight would render a
|
||||||
|
// synthesised bold, a smeared regular in print, so the
|
||||||
|
// weight moves to the nearest one it really has.
|
||||||
|
weight: nearestWeight(id, weight) as BookWeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
title={face.name}
|
||||||
|
>
|
||||||
|
{face.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<Line label={t('journey.studio.weight')}>
|
||||||
|
<Choice
|
||||||
|
value={weight}
|
||||||
|
options={WEIGHTS.map((w) => ({
|
||||||
|
value: w,
|
||||||
|
label: String(w),
|
||||||
|
disabled: !hasWeight(font, w),
|
||||||
|
title: hasWeight(font, w) ? undefined : t('journey.studio.weightMissing'),
|
||||||
|
}))}
|
||||||
|
onPick={onPick}
|
||||||
|
/>
|
||||||
|
</Line>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user