diff --git a/crates/morpho-core/src/engine.rs b/crates/morpho-core/src/engine.rs
index bf07718..e17d7e4 100644
--- a/crates/morpho-core/src/engine.rs
+++ b/crates/morpho-core/src/engine.rs
@@ -1712,6 +1712,7 @@ impl Engine {
cap_height: self.font_metric(spec, upm, |i| i.cap_height)?,
x_height: self.font_metric(spec, upm, |i| i.x_height)?,
weight_class: weight,
+ italic: spec.italic.unwrap_or(slant.abs() > 0.5),
italic_angle: -slant,
copyright,
description: format!("Derived from {} with Morphotype.", sources.iter().map(|i| i.full_name.as_str()).collect::>().join(" and ")),
@@ -1853,8 +1854,12 @@ pub struct FontSpec {
pub style: String,
#[serde(default)]
pub charset: Option,
+ /// usWeightClass; worked out from the Weight modifiers when absent.
#[serde(default)]
pub weight_class: Option,
+ /// An italic style; when absent, italic if the look slants.
+ #[serde(default)]
+ pub italic: Option,
#[serde(default)]
pub quality: Option,
#[serde(default)]
diff --git a/crates/morpho-core/src/ttf.rs b/crates/morpho-core/src/ttf.rs
index 038287c..7ea0941 100644
--- a/crates/morpho-core/src/ttf.rs
+++ b/crates/morpho-core/src/ttf.rs
@@ -19,6 +19,9 @@ pub struct FontMeta {
pub cap_height: i16,
pub x_height: i16,
pub weight_class: u16,
+ /// An italic style (fsSelection, macStyle, the name and STAT tables),
+ /// whether or not it leans.
+ pub italic: bool,
/// Degrees, negative leans right (OpenType convention).
pub italic_angle: f64,
pub copyright: String,
@@ -325,7 +328,7 @@ fn weight_name(class: u16) -> &'static str {
/// STAT 1.1 with wght and ital axes and this font's place on each, so apps
/// can group and name the style (Regular and Roman are elidable).
fn stat_table(m: &FontMeta) -> Vec {
- let italic = m.italic_angle.abs() > 0.5;
+ let italic = m.italic;
let mut w = W(Vec::new());
w.u16(1);
w.u16(1);
@@ -361,7 +364,7 @@ fn name_table(m: &FontMeta) -> Vec {
recs.push((2, m.style.clone()));
} else {
recs.push((1, format!("{} {}", m.family, m.style)));
- recs.push((2, "Regular".into()));
+ recs.push((2, if m.italic { "Italic" } else { "Regular" }.into()));
}
recs.push((3, format!("{};MRPH;{}", m.version, ps_name(&m.family, &m.style))));
recs.push((4, if m.style == "Regular" { m.family.clone() } else { format!("{} {}", m.family, m.style) }));
@@ -377,7 +380,7 @@ fn name_table(m: &FontMeta) -> Vec {
}
}
// Names the STAT table refers to.
- let italic = m.italic_angle.abs() > 0.5;
+ let italic = m.italic;
recs.push((STAT_NAMES, "Weight".into()));
recs.push((STAT_NAMES + 1, "Italic".into()));
recs.push((STAT_NAMES + 2, weight_name(m.weight_class).into()));
@@ -1078,7 +1081,7 @@ pub fn write_ttf(m: &FontMeta, glyphs: &[OutGlyph]) -> Vec {
head.i16(fx1);
head.i16(fy1);
let bold = m.weight_class >= 600;
- let italic = m.italic_angle.abs() > 0.5;
+ let italic = m.italic;
head.u16((bold as u16) | ((italic as u16) << 1));
head.u16(8);
head.i16(2);
@@ -1095,8 +1098,9 @@ pub fn write_ttf(m: &FontMeta, glyphs: &[OutGlyph]) -> Vec {
hhea.i16(min_rsb);
hhea.i16(max_extent);
let slope = m.italic_angle.to_radians();
- hhea.i16(if italic { 1000 } else { 1 });
- hhea.i16(if italic { clamp16(-slope.tan() * 1000.0) } else { 0 });
+ let slanted = m.italic_angle.abs() > 0.5;
+ hhea.i16(if slanted { 1000 } else { 1 });
+ hhea.i16(if slanted { clamp16(-slope.tan() * 1000.0) } else { 0 });
hhea.i16(0);
for _ in 0..4 {
hhea.i16(0);
@@ -1316,6 +1320,7 @@ mod tests {
cap_height: 700,
x_height: 500,
weight_class: 400,
+ italic: false,
italic_angle: 0.0,
copyright: "test".into(),
description: String::new(),
@@ -1371,6 +1376,42 @@ mod tests {
assert_eq!(fam.as_deref(), Some("Test Morph"));
}
+ #[test]
+ fn style_sets_weight_and_italic_flags() {
+ let glyphs = vec![OutGlyph { codepoint: Some('A' as u32), advance: 600.0, contours: vec![square(50.0, 500.0)] }];
+ let names = |bytes: &[u8]| {
+ let face = Face::parse(bytes, 0).unwrap();
+ let get = |id: u16| face.names().into_iter().find(|n| n.name_id == id).and_then(|n| n.to_string());
+ (get(1), get(2), get(17))
+ };
+ let head_mac_style = |bytes: &[u8]| {
+ let head = Face::parse(bytes, 0).unwrap().raw_face().table(rustybuzz::ttf_parser::Tag::from_bytes(b"head")).unwrap();
+ u16::from_be_bytes([head[44], head[45]])
+ };
+
+ // An upright italic: flagged italic though it doesn't lean.
+ let mut m = meta();
+ m.style = "Light Italic".into();
+ m.weight_class = 300;
+ m.italic = true;
+ let bytes = write_ttf(&m, &glyphs);
+ let face = Face::parse(&bytes, 0).unwrap();
+ assert_eq!(face.weight().to_number(), 300);
+ assert!(face.is_italic() && !face.is_bold() && !face.is_regular());
+ assert_eq!(face.italic_angle(), 0.0);
+ assert_eq!(head_mac_style(&bytes), 0b10);
+ assert_eq!(names(&bytes), (Some("Test Morph Light Italic".into()), Some("Italic".into()), Some("Light Italic".into())));
+
+ let mut m = meta();
+ m.style = "Bold".into();
+ m.weight_class = 700;
+ let bytes = write_ttf(&m, &glyphs);
+ let face = Face::parse(&bytes, 0).unwrap();
+ assert!(face.is_bold() && !face.is_italic());
+ assert_eq!(head_mac_style(&bytes), 0b01);
+ assert_eq!(names(&bytes), (Some("Test Morph".into()), Some("Bold".into()), None));
+ }
+
/// Writes kerning between `n` square glyphs and shapes every pair back.
fn kerning_roundtrip(n: u32, kern: &dyn Fn(u32, u32) -> i16) -> (usize, usize) {
let ch = |i: u32| char::from_u32(0x100 + i).unwrap();
diff --git a/ui/src/app/fontStyles.test.ts b/ui/src/app/fontStyles.test.ts
new file mode 100644
index 0000000..6aa433a
--- /dev/null
+++ b/ui/src/app/fontStyles.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest';
+import { canonicalStyle, isStandardStyle, lookWeight, parseStyle, STANDARD_STYLES, styleName } from './fontStyles';
+import { fontFaceCss, specOf } from './typeActions';
+import type { ModEntry } from '../engine/types';
+import type { TypeProject } from '../state/model';
+
+const font = (style: string, modifiers: ModEntry[] = []): TypeProject => ({ family: 'S', style, base: 'inter', axes: {}, morph: null, modifiers, tracking: 0, charset: 'A', glyphs: {}, kerning: {}, sample: '' });
+const weightMod = (amount: number, on = true) => ({ kind: 'weight', amount, on, uid: 'm' }) as ModEntry;
+
+describe('font styles', () => {
+ it('lists nine weights, upright and italic', () => {
+ expect(STANDARD_STYLES).toHaveLength(18);
+ expect(STANDARD_STYLES.slice(0, 9)).toEqual(['Thin', 'ExtraLight', 'Light', 'Regular', 'Medium', 'SemiBold', 'Bold', 'ExtraBold', 'Black']);
+ expect(STANDARD_STYLES).toContain('Italic');
+ expect(STANDARD_STYLES).toContain('Bold Italic');
+ expect(STANDARD_STYLES).not.toContain('Regular Italic');
+ });
+
+ it('round-trips every standard style', () => {
+ for (const name of STANDARD_STYLES) expect(styleName(parseStyle(name)!)).toBe(name);
+ });
+
+ it('maps names to weight and italic', () => {
+ expect(parseStyle('Regular')).toEqual({ weight: 400, italic: false });
+ expect(parseStyle('Italic')).toEqual({ weight: 400, italic: true });
+ expect(parseStyle('ExtraLight')).toEqual({ weight: 200, italic: false });
+ expect(parseStyle('Extra Bold Italic')).toEqual({ weight: 800, italic: true });
+ expect(parseStyle('semi-bold')).toEqual({ weight: 600, italic: false });
+ expect(parseStyle('Demibold')).toEqual({ weight: 600, italic: false });
+ expect(parseStyle('Heavy Oblique')).toEqual({ weight: 900, italic: true });
+ expect(parseStyle('Hairline')).toEqual({ weight: 100, italic: false });
+ expect(parseStyle('Condensed Bold')).toEqual({ weight: 700, italic: false });
+ expect(parseStyle('Display')).toBeNull();
+ expect(parseStyle('')).toBeNull();
+ });
+
+ it('spells a base font style the standard way, keeping custom ones', () => {
+ expect(canonicalStyle('Semi Bold')).toBe('SemiBold');
+ expect(canonicalStyle('Regular Italic')).toBe('Italic');
+ expect(canonicalStyle('bold italic')).toBe('Bold Italic');
+ expect(canonicalStyle('Condensed Bold')).toBe('Condensed Bold');
+ expect(canonicalStyle('Display')).toBe('Display');
+ expect(isStandardStyle('Display')).toBe(false);
+ });
+
+ it('reckons a look weight like the engine', () => {
+ expect(lookWeight([])).toBe(400);
+ expect(lookWeight([weightMod(0.075)])).toBe(700);
+ expect(lookWeight([weightMod(0.075, false)])).toBe(400);
+ expect(lookWeight([weightMod(-1)])).toBe(100);
+ expect(lookWeight([weightMod(1)])).toBe(900);
+ expect(lookWeight([weightMod(0.05)], 300)).toBe(500);
+ });
+});
+
+describe('style in the export', () => {
+ it('sends the weight class and italic flag of a named style', () => {
+ expect(specOf(font('Light Italic'))).toMatchObject({ style: 'Light Italic', weightClass: 300, italic: true });
+ expect(specOf(font('Bold'))).toMatchObject({ weightClass: 700, italic: false });
+ });
+
+ it('leaves a custom style to the look', () => {
+ const spec = specOf(font('Display'));
+ expect(spec.weightClass).toBeUndefined();
+ expect(spec.italic).toBeUndefined();
+ });
+
+ it('writes @font-face weight and style', () => {
+ expect(fontFaceCss(font('ExtraLight'), 'S')).toContain('font-weight: 200;');
+ expect(fontFaceCss(font('ExtraBold Italic'), 'S')).toMatch(/font-weight: 800;[\s\S]*font-style: italic;/);
+ expect(fontFaceCss(font('Display', [weightMod(0.075)]), 'S')).toContain('font-weight: 700;');
+ });
+});
diff --git a/ui/src/app/fontStyles.ts b/ui/src/app/fontStyles.ts
new file mode 100644
index 0000000..b459685
--- /dev/null
+++ b/ui/src/app/fontStyles.ts
@@ -0,0 +1,75 @@
+// Font style names (the subfamily, "Bold Italic") and the weight class and
+// italic flag they stand for in an exported font.
+import type { ModEntry } from '../engine/types';
+
+export const WEIGHTS: { weight: number; name: string }[] = [
+ { weight: 100, name: 'Thin' },
+ { weight: 200, name: 'ExtraLight' },
+ { weight: 300, name: 'Light' },
+ { weight: 400, name: 'Regular' },
+ { weight: 500, name: 'Medium' },
+ { weight: 600, name: 'SemiBold' },
+ { weight: 700, name: 'Bold' },
+ { weight: 800, name: 'ExtraBold' },
+ { weight: 900, name: 'Black' },
+];
+
+export interface FontStyle {
+ weight: number;
+ italic: boolean;
+}
+
+/** The standard name of a weight and slope: "Regular", "Italic", "Light Italic". */
+export function styleName({ weight, italic }: FontStyle): string {
+ const w = WEIGHTS.find((x) => x.weight === weight)?.name ?? 'Regular';
+ if (!italic) return w;
+ return w === 'Regular' ? 'Italic' : `${w} Italic`;
+}
+
+/** Every standard style, uprights first. */
+export const STANDARD_STYLES: string[] = [false, true].flatMap((italic) => WEIGHTS.map((w) => styleName({ weight: w.weight, italic })));
+
+export const isStandardStyle = (name: string) => STANDARD_STYLES.includes(name);
+
+// Longer words first, so "ExtraBold" isn't read as Bold.
+const WEIGHT_WORDS: [RegExp, number][] = [
+ [/(extra|ultra)light/, 200],
+ [/(extra|ultra)bold/, 800],
+ [/(semi|demi)bold/, 600],
+ [/thin|hairline/, 100],
+ [/light/, 300],
+ [/medium/, 500],
+ [/bold/, 700],
+ [/black|heavy/, 900],
+ [/regular|normal|book|roman/, 400],
+];
+
+/**
+ * The weight and slope a style name stands for, also for spellings like
+ * "Semi Bold" or "Heavy Oblique". Null when it names neither (a custom
+ * style such as "Display").
+ */
+export function parseStyle(name: string): FontStyle | null {
+ const s = name.toLowerCase().replace(/[\s_-]+/g, '');
+ const italic = /italic|oblique/.test(s);
+ const weight = WEIGHT_WORDS.find(([re]) => re.test(s))?.[1];
+ if (weight === undefined && !italic) return null;
+ return { weight: weight ?? 400, italic };
+}
+
+/** The standard spelling of a style name ("semi bold italic" → "SemiBold Italic"), or the name as it is. */
+export function canonicalStyle(name: string): string {
+ const p = parseStyle(name);
+ const compact = (s: string) => s.toLowerCase().replace(/[\s_-]+/g, '').replace(/^regular(?=italic)/, '');
+ return p && compact(styleName(p)) === compact(name) ? styleName(p) : name.trim();
+}
+
+/**
+ * The weight a font look adds up to, as the engine reckons it when the
+ * style doesn't say: `from` (400) plus 4000 per em of Weight, to the
+ * nearest 100.
+ */
+export function lookWeight(mods: ModEntry[], from = 400): number {
+ const w = mods.reduce((acc, m) => (m.on && m.kind === 'weight' ? acc + m.amount * 4000 : acc), from);
+ return Math.min(9, Math.max(1, Math.round(w / 100))) * 100;
+}
diff --git a/ui/src/app/typeActions.ts b/ui/src/app/typeActions.ts
index b5a80af..92abd1c 100644
--- a/ui/src/app/typeActions.ts
+++ b/ui/src/app/typeActions.ts
@@ -8,6 +8,7 @@ import { allLayers } from '../state/tree';
import { edit, store, toast, undo } from '../state/store';
import { addShape, errorText, primaryNode, registerFontEntry, slug } from './actions';
import { zip } from './zip';
+import { canonicalStyle, lookWeight, parseStyle } from './fontStyles';
import { addClass, deleteClass, joinClass, leaveClass, resolveKerning, setClassMembers, setEntry } from './kerning';
export const CHARSETS = {
@@ -48,13 +49,31 @@ export function newFontDefaults(): NewFontOptions {
};
}
+/** A font's own style name ("Black", "SemiBold"), standard spelling where it has one. */
+export function baseStyle(fontId: string): string {
+ const style = store.get().fonts.find((f) => f.id === fontId)?.info.style.trim();
+ return style ? canonicalStyle(style) : 'Regular';
+}
+
+/**
+ * Reshape a different font. A style still matching the old base's follows
+ * the new one; a style the user chose stays.
+ */
+export function setTypeBase(fontId: string) {
+ editType((tp) => {
+ if (!tp.blank && tp.style === baseStyle(tp.base)) tp.style = baseStyle(fontId);
+ tp.base = fontId;
+ tp.axes = {};
+ });
+}
+
/** Start a font project. */
export function createTypeProject(o: NewFontOptions): TypeProject {
const tp: TypeProject = {
id: uid('f'),
blank: o.blank || undefined,
family: o.family.trim() || 'Untitled Sans',
- style: 'Regular',
+ style: o.blank ? 'Regular' : baseStyle(o.base),
base: o.base,
axes: o.axes ?? {},
morph: o.blank ? null : (o.morph ?? null),
@@ -99,6 +118,10 @@ export function specOf(t: TypeProject, opts: { quality?: number; format?: 'ttf'
modifiers: t.modifiers,
family: t.family,
style: t.style,
+ // A named style sets the weight class and italic flags; a custom name
+ // leaves them to the look.
+ weightClass: parseStyle(t.style)?.weight,
+ italic: parseStyle(t.style)?.italic,
charset: t.charset,
glyphs,
tracking: t.tracking,
@@ -228,14 +251,14 @@ export async function exportFontFile(format: 'ttf' | 'woff') {
}
export function fontFaceCss(t: TypeProject, base: string) {
- const weight = t.style.match(/thin|light|regular|medium|semi|bold|black|heavy/i)?.[0]?.toLowerCase();
- const w = { thin: 100, light: 300, regular: 400, medium: 500, semi: 600, bold: 700, black: 900, heavy: 900 }[weight ?? 'regular'] ?? 400;
+ const style = parseStyle(t.style);
+ const w = style?.weight ?? lookWeight(t.modifiers);
return `@font-face {
font-family: "${t.family}";
src: url("${base}.woff") format("woff"),
url("${base}.ttf") format("truetype");
font-weight: ${w};
- font-style: ${/italic|oblique/i.test(t.style) ? 'italic' : 'normal'};
+ font-style: ${style?.italic ? 'italic' : 'normal'};
font-display: swap;
}
`;
diff --git a/ui/src/components/type/StylePicker.tsx b/ui/src/components/type/StylePicker.tsx
new file mode 100644
index 0000000..be88d1a
--- /dev/null
+++ b/ui/src/components/type/StylePicker.tsx
@@ -0,0 +1,106 @@
+// The font's style name: the standard styles by weight, upright and italic,
+// or a name of your own. A standard style sets the weight class and italic
+// flags apps use to group a family.
+import { useState } from 'react';
+import { isStandardStyle, lookWeight, parseStyle, styleName, WEIGHTS } from '../../app/fontStyles';
+import type { ModEntry } from '../../engine/types';
+import { Icon } from '../icons';
+import { Menu } from '../ui';
+
+export function StylePicker({ value, onChange, base, mods }: { value: string; onChange: (style: string) => void; base?: string; mods: ModEntry[] }) {
+ const [naming, setNaming] = useState(false);
+ const custom = naming || !isStandardStyle(value);
+ const parsed = parseStyle(value);
+ const look = lookWeight(mods);
+ // A look that thickens or thins the base suggests the style it lands on.
+ const weighted = lookWeight(mods, parseStyle(base ?? '')?.weight ?? 400);
+ const thickens = mods.some((m) => m.on && m.kind === 'weight' && m.amount !== 0);
+ const suggest = parsed && !custom && thickens && Math.abs(weighted - parsed.weight) >= 200 ? styleName({ weight: weighted, italic: parsed.italic }) : null;
+ const item = (name: string, close: () => void) => (
+ {
+ setNaming(false);
+ onChange(name);
+ close();
+ }}
+ >
+ {!custom && value === name && }
+ {name}
+ {name === base && base }
+
+ );
+ return (
+ <>
+
+
Style
+
+ {custom ? 'Custom' : value}
+ {parsed && {parsed.weight} }
+
+ >
+ }
+ >
+ {(close) => (
+
+
+
+
Upright
+
Italic
+ {WEIGHTS.map((w) => (
+
+ {w.weight}
+ {item(styleName({ weight: w.weight, italic: false }), close)}
+ {item(styleName({ weight: w.weight, italic: true }), close)}
+
+ ))}
+
+
+
{
+ setNaming(true);
+ close();
+ }}
+ >
+ {custom ? : }
+ Custom name…
+
+
+ )}
+
+
+ {custom && (
+
+ Name
+ onChange(e.target.value)} />
+
+ )}
+
+ {!parsed
+ ? `A custom name. Its weight comes from the look (${look}) and it's italic if the look slants.`
+ : `${custom ? 'Reads as' : 'Exports as'} weight ${parsed.weight}, ${parsed.italic ? 'italic' : 'upright'}. Apps group a family's styles by these.`}
+ {suggest && (
+ <>
+ {' '}
+ Your look is closer to {suggest}.{' '}
+ onChange(suggest)}>
+ Use {suggest}
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/ui/src/components/type/TypeMode.tsx b/ui/src/components/type/TypeMode.tsx
index 900b6f2..f813755 100644
--- a/ui/src/components/type/TypeMode.tsx
+++ b/ui/src/components/type/TypeMode.tsx
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { importImage } from '../../app/actions';
-import { addCharacters, addGlyphPart, CHARSETS, closeTypeProject, customFontId, customizeGlyph, editType, exportFontFile, exportWebKit, installOnMac, removeCharacter, resetGlyph, specOf } from '../../app/typeActions';
+import { addCharacters, addGlyphPart, baseStyle, CHARSETS, closeTypeProject, customFontId, customizeGlyph, editType, exportFontFile, exportWebKit, installOnMac, removeCharacter, resetGlyph, setTypeBase, specOf } from '../../app/typeActions';
+import { StylePicker } from './StylePicker';
import { TypeStart } from './TypeStart';
import { GlyphLayers, GlyphLook, GlyphPartPanel, PartsOverlay } from './GlyphLayers';
import { TYPE_TERMS } from './TypeGuide';
@@ -550,13 +551,10 @@ function FontTab({ t }: { t: TypeProject }) {
-
- Style
- editType((tp) => (tp.style = e.target.value), 'style')} />
-
+ editType((tp) => (tp.style = v), 'style')} />
{t.blank ? 'Lines from' : 'Base font'}
- id && editType((tp) => ((tp.base = id), (tp.axes = {})))} />
+ id && setTypeBase(id)} />
{!t.blank && (
<>
diff --git a/ui/src/components/type/UseFontDialog.tsx b/ui/src/components/type/UseFontDialog.tsx
index 81f4c9d..f5384ec 100644
--- a/ui/src/components/type/UseFontDialog.tsx
+++ b/ui/src/components/type/UseFontDialog.tsx
@@ -2,6 +2,7 @@
// from its base, and choose what happens to text set in the base font.
import { useState } from 'react';
import { customFontId, editType, fontChanges, nameClash, useInDesign, type UseMode } from '../../app/typeActions';
+import { STANDARD_STYLES } from '../../app/fontStyles';
import { allLayers } from '../../state/tree';
import { store, useStore } from '../../state/store';
import { Icon } from '../icons';
@@ -58,7 +59,12 @@ export function UseFontDialog() {
Style
- setStyle(e.target.value)} spellCheck={false} />
+ setStyle(e.target.value)} spellCheck={false} list="font-styles" />
+
+ {STANDARD_STYLES.map((s) => (
+
+ ))}
+
{clash && {clash}
}
diff --git a/ui/src/components/ui.tsx b/ui/src/components/ui.tsx
index 18d05bc..fc8112a 100644
--- a/ui/src/components/ui.tsx
+++ b/ui/src/components/ui.tsx
@@ -118,7 +118,7 @@ export function Popover({
// Arrow keys move between menu items.
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
- const items = Array.from(ref.current?.querySelectorAll('[role=menuitem]:not([disabled])') ?? []);
+ const items = Array.from(ref.current?.querySelectorAll('[role^=menuitem]:not([disabled])') ?? []);
if (!items.length) return;
e.preventDefault();
const i = items.indexOf(document.activeElement as HTMLElement);
diff --git a/ui/src/engine/types.ts b/ui/src/engine/types.ts
index 39362ec..9f9726f 100644
--- a/ui/src/engine/types.ts
+++ b/ui/src/engine/types.ts
@@ -360,7 +360,10 @@ export interface FontSpec {
family: string;
style: string;
charset: string | null;
+ /** usWeightClass; from the look's Weight modifiers when absent. */
weightClass?: number;
+ /** An italic style; when absent, italic if the look slants. */
+ italic?: boolean;
quality?: number;
glyphs?: GlyphEdit[];
tracking?: number;
diff --git a/ui/src/styles.css b/ui/src/styles.css
index 01d8809..74741ea 100644
--- a/ui/src/styles.css
+++ b/ui/src/styles.css
@@ -5261,3 +5261,42 @@ body.tl-dragging * {
.button-row {
gap: 6px;
}
+
+/* Type mode style picker: weights down, upright and italic across. */
+.style-select .style-num {
+ color: var(--dk-label);
+ font-family: var(--mono);
+ font-size: 11px;
+}
+.style-menu .style-grid {
+ display: grid;
+ grid-template-columns: 34px 1fr 1fr;
+ column-gap: 2px;
+}
+.style-grid .style-row {
+ display: contents;
+}
+.style-grid > .menu-label {
+ padding-left: 30px;
+}
+.style-weight {
+ align-self: center;
+ justify-self: end;
+ padding-right: 4px;
+ color: var(--faint);
+ font-family: var(--mono);
+ font-size: 11px;
+}
+.style-base {
+ color: var(--faint);
+ font-size: 10.5px;
+}
+.menu-item:hover .style-base {
+ color: inherit;
+}
+.text-field.style-name {
+ width: auto;
+ min-width: 0;
+ max-width: 65%;
+ text-align: right;
+}