Font Sizes, per role, per width
A fifth section between Fonts and Spacing. The same four roles the families use, so a template asks for a role and gets both — and a role used at two sizes on purpose collapses to one, which was the trade taken over a separate list of text roles. The live preview builds the same stylesheet the site renders, because a size is three values and an inline style cannot carry a media query. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,12 @@ import { readableError } from '../errors';
|
||||
import { useFlash } from '../useFlash';
|
||||
import { useAutosave } from '../useAutosave';
|
||||
import { DEVICES, usePreviewable, useViewMode } from '../ViewMode';
|
||||
import { applySpacing, paintMeasure, type SpacingDraft } from '../canvas';
|
||||
import { applyFontSizes, applySpacing, paintMeasure, type SpacingDraft } from '../canvas';
|
||||
import { SitePreview, useDevPort } from '../SitePreview';
|
||||
import { useEditorSlot } from '../useEditorSlot';
|
||||
import { type LayoutBand } from './PaddingTab';
|
||||
import { SpacingTab } from './SpacingTab';
|
||||
import { FontSizesTab, type FontSizes } from './FontSizesTab';
|
||||
|
||||
/**
|
||||
* The shell every page renders inside: the menu, the footer, the colours and the typefaces.
|
||||
@@ -59,6 +60,12 @@ type Layout = {
|
||||
padding_steps: number[];
|
||||
/** Where the space goes, band by band. See SpacingTab, and the site's own spacingClasses. */
|
||||
spacing: SpacingDraft;
|
||||
/** How big each role is, per breakpoint. See FontSizesTab. */
|
||||
font_sizes: FontSizes;
|
||||
/** The type scale, sent by the API so this file holds no copy of it. */
|
||||
text_steps: string[];
|
||||
/** Each step's length, so the live preview can build the same CSS the site would. */
|
||||
text_sizes: Record<string, string>;
|
||||
/**
|
||||
* Which face plays each role when nothing has been chosen, read from the site's own stylesheet.
|
||||
*
|
||||
@@ -110,7 +117,7 @@ const themeProperties = (draft: Layout): Record<string, string | null> => ({
|
||||
});
|
||||
|
||||
/** Every section, open from the start. The values below must match the AccordionItems. */
|
||||
const SECTIONS_OPEN = ['colours', 'typefaces', 'padding'];
|
||||
const SECTIONS_OPEN = ['colours', 'typefaces', 'sizes', 'padding'];
|
||||
|
||||
/**
|
||||
* A section's title.
|
||||
@@ -124,6 +131,36 @@ const SECTIONS_OPEN = ['colours', 'typefaces', 'padding'];
|
||||
*/
|
||||
const SECTION_TITLE = 'text-sm font-semibold tracking-wide';
|
||||
|
||||
/**
|
||||
* The chosen sizes as CSS.
|
||||
*
|
||||
* Mirrors `fontSizeRules` in the site's layout-settings.ts, and is a copy for the same reason
|
||||
* `themeProperties` is: showing a change before it is saved means producing it here. The check is the
|
||||
* same — save, and if the live preview and the saved page disagree, this copy is the wrong one.
|
||||
*
|
||||
* The lengths come from the API rather than a list held here, so only the SHAPE is duplicated.
|
||||
*/
|
||||
const fontSizeRules = (sizes: FontSizes, lengths: Record<string, string>): string => {
|
||||
const queries = [
|
||||
['base', null],
|
||||
['md', '768px'],
|
||||
['lg', '1024px'],
|
||||
] as const;
|
||||
|
||||
return queries
|
||||
.map(([breakpoint, width]) => {
|
||||
const declarations = (['title', 'body', 'sub', 'display'] as const)
|
||||
.map((role) => [role, lengths[sizes[role]?.[breakpoint] ?? '']] as const)
|
||||
.filter(([, length]) => length)
|
||||
.map(([role, length]) => `--text-${role}:${length}`)
|
||||
.join(';');
|
||||
|
||||
if (!declarations) return '';
|
||||
return width ? `@media (min-width:${width}){:root{${declarations}}}` : `:root{${declarations}}`;
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
const COLOURS = [
|
||||
{ key: 'background' as const, label: 'Background', fallback: '#ffffff' },
|
||||
{ key: 'foreground' as const, label: 'Text', fallback: '#000000' },
|
||||
@@ -170,6 +207,7 @@ export const LayoutPanel = () => {
|
||||
*/
|
||||
const WRITABLE = [
|
||||
'spacing',
|
||||
'font_sizes',
|
||||
'background',
|
||||
'foreground',
|
||||
'accent',
|
||||
@@ -226,17 +264,20 @@ export const LayoutPanel = () => {
|
||||
// Serialised, so the callback is rebuilt when a NUMBER changes rather than when the object identity
|
||||
// does — a fresh `{}` every render would re-apply the whole preview on every keystroke elsewhere.
|
||||
const spacingKey = JSON.stringify(draft?.spacing ?? {});
|
||||
const sizesCss = draft ? fontSizeRules(draft.font_sizes, draft.text_sizes) : '';
|
||||
|
||||
const onDocument = useCallback(
|
||||
(frameDocument: Document) => {
|
||||
const measure = paintMeasure(frameDocument);
|
||||
const space = applySpacing(frameDocument, JSON.parse(spacingKey) as SpacingDraft);
|
||||
const sizes = applyFontSizes(frameDocument, sizesCss);
|
||||
return () => {
|
||||
sizes();
|
||||
space();
|
||||
measure();
|
||||
};
|
||||
},
|
||||
[spacingKey],
|
||||
[spacingKey, sizesCss],
|
||||
);
|
||||
|
||||
const [, setEditor] = useEditorSlot();
|
||||
@@ -346,6 +387,20 @@ export const LayoutPanel = () => {
|
||||
</>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="sizes" className="rounded-lg border border-border px-4">
|
||||
<AccordionTrigger className={SECTION_TITLE}>
|
||||
<span>
|
||||
Font Sizes <span className="font-normal text-muted-foreground">({DEVICES[device].label})</span>
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<FontSizesTab
|
||||
sizes={draft.font_sizes}
|
||||
steps={draft.text_steps}
|
||||
onChange={(font_sizes) => patch({ font_sizes })}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="padding" className="rounded-lg border border-border px-4">
|
||||
<AccordionTrigger className={SECTION_TITLE}>
|
||||
{/*
|
||||
|
||||
Reference in New Issue
Block a user