Self-hosted WOFF2 bundle lands at src/shared/fonts/ per D-05 typography
pairing with R2 Newsreader→Lora substitution (designer reply 2026-05-19).
Bundle composition (8 WOFF2 files; ~236 KB total):
- Lora-VariableFont.woff2 (49 KB) — display family, normal style, wght
axis 400-700; Cyreal foundry (cyrealtype/Lora-Cyrillic main branch)
- Lora-Italic-VariableFont.woff2 (53 KB) — display family italic, wght
400-700; separate variable file per upstream layout (A5 verified at
execute time: Lora-Cyrillic ships italic as its own variable file).
- IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2 (24/25/25/23 KB) —
UI body family with Latin + Cyrillic basic
- IBMPlexMono-{Regular,Medium}.woff2 (15 KB each) — diagnostic / timer
family
Companion artifacts:
- LICENSE-Lora.txt — verbatim OFL.txt + Lora Project Authors copyright
- LICENSE-IBM-Plex.txt — verbatim LICENSE.txt + IBM Corp. copyright
- README.md — substantive (160 lines): bundle table, R2 rationale,
subset coverage (Cyrillic basic + supplements + №), regeneration recipe
with one-off curl commands, MV3 CSP self-host rationale.
scripts/subset-fonts.sh (130 lines):
- One-off subsetting recipe; takes a scratch dir of upstream TTFs.
- UNICODES range: U+0020-007E + U+00A0-00FF + Cyrillic basic
(U+0400-045F) + Ukrainian (Ґґ) + Kazakh (Ұұ) + № sign.
- Common pyftsubset flags shared across faces; per-face subset_face
helper. Documents source URLs in usage block.
Bundle is sufficient for the 12 i18n keys (Wave 3) + welcome hero
(Plan 01-10 conditional) per the Brief §02 Russian copy specified in
.planning/intel/brand-decisions-v1.md.
Verification: tests/build/fonts-present.test.ts is 9/10 GREEN (1 RED
remaining is the tokens.css existence check, which is Wave 1 Task 2's
job). Existing 100/100 vitest baseline preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Bash
Executable File
135 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# scripts/subset-fonts.sh — Plan 01-12 Wave 1 Task 1.
|
|
#
|
|
# One-off subsetting recipe for the Mokosh design-integration font bundle:
|
|
#
|
|
# Lora variable (normal + italic, axis wght 400-700) — display family per
|
|
# R2 designer reply 2026-05-19 (substitutes Newsreader for Cyrillic
|
|
# coverage; Cyreal foundry, OFL-1.1)
|
|
# IBM Plex Sans (Regular, Medium, SemiBold, Bold) — UI body family
|
|
# IBM Plex Mono (Regular, Medium) — diagnostic / timer family
|
|
#
|
|
# All faces are subsetted to Latin (U+0020-007E + U+00A0-00FF) + Cyrillic
|
|
# basic (U+0400-045F + supplemental code points) per RESEARCH §1 + §6.
|
|
# Total bundled artifact target: ~250-300 KB per R2 substitution adjustment.
|
|
#
|
|
# Upstream sources:
|
|
# Lora-Cyrillic: https://github.com/cyrealtype/Lora-Cyrillic (main branch)
|
|
# IBM Plex: https://github.com/IBM/plex (master branch)
|
|
#
|
|
# Output: src/shared/fonts/*.woff2 (7 or 8 files depending on Lora italic
|
|
# upstream layout — A5 in RESEARCH Assumptions Log).
|
|
#
|
|
# Re-run trigger: when the upstream Lora-Cyrillic or IBM/plex releases
|
|
# bump a major version with bug fixes; or when the UNICODES range needs
|
|
# expansion (Cyrillic supplement, extra punctuation). The committed
|
|
# WOFF2 files are the source of truth — running this script overwrites
|
|
# them only on intentional regeneration.
|
|
#
|
|
# References:
|
|
# - fontTools pyftsubset:
|
|
# https://fonttools.readthedocs.io/en/latest/subset/index.html
|
|
# - WOFF2 (RFC 8081): https://www.rfc-editor.org/rfc/rfc8081
|
|
# - OFL-1.1 best practices: https://scripts.sil.org/OFL_web
|
|
#
|
|
# Google bash style applies; see https://google.github.io/styleguide/shellguide.html
|
|
|
|
set -euo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
readonly FONTS_OUT_DIR="${REPO_ROOT}/src/shared/fonts"
|
|
|
|
# Subset coverage per RESEARCH §1 + §6:
|
|
# Latin core U+0020-007E
|
|
# Latin supplement U+00A0-00FF (covers Cyrillic context: nbsp, ©, ®, ™ etc.)
|
|
# Latin extended bits U+0131 (dotless i), U+0152-0153 (Œœ), U+0301 (combining acute)
|
|
# Cyrillic basic U+0400-045F (full RU + UA basic alphabet)
|
|
# Cyrillic supplement U+0490-0491 (Ґґ), U+04B0-04B1 (Ұұ — Kazakh)
|
|
# Numero sign U+2116 (№ — common Russian punctuation)
|
|
readonly UNICODES='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116'
|
|
|
|
# Common pyftsubset flags shared across faces.
|
|
readonly -a PYFTSUBSET_FLAGS=(
|
|
"--unicodes=${UNICODES}"
|
|
'--flavor=woff2'
|
|
"--layout-features=*"
|
|
'--no-hinting'
|
|
'--desubroutinize'
|
|
'--name-IDs=*'
|
|
'--name-legacy'
|
|
'--name-languages=*'
|
|
)
|
|
|
|
# subset_face <input-ttf> <output-woff2>
|
|
subset_face() {
|
|
local -r input_ttf="$1"
|
|
local -r output_woff2="$2"
|
|
if [[ ! -f "${input_ttf}" ]]; then
|
|
echo "ERROR: input TTF not found: ${input_ttf}" >&2
|
|
return 1
|
|
fi
|
|
pyftsubset "${input_ttf}" \
|
|
"${PYFTSUBSET_FLAGS[@]}" \
|
|
"--output-file=${output_woff2}"
|
|
local -r bytes="$(stat -c%s "${output_woff2}")"
|
|
echo " subsetted: ${output_woff2} (${bytes} bytes)"
|
|
}
|
|
|
|
main() {
|
|
local -r scratch="${1:-}"
|
|
if [[ -z "${scratch}" || ! -d "${scratch}" ]]; then
|
|
cat >&2 <<EOF
|
|
Usage: $(basename "$0") <scratch-dir>
|
|
|
|
<scratch-dir> must contain the upstream TTFs:
|
|
Lora-VariableFont_wght.ttf
|
|
Lora-Italic-VariableFont_wght.ttf
|
|
IBMPlexSans-Regular.ttf, -Medium.ttf, -SemiBold.ttf, -Bold.ttf
|
|
IBMPlexMono-Regular.ttf, -Medium.ttf
|
|
|
|
Fetch recipe:
|
|
SCRATCH=\$(mktemp -d)
|
|
curl -L -o "\${SCRATCH}/Lora-VariableFont_wght.ttf" \\
|
|
"https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/variable/Lora%5Bwght%5D.ttf"
|
|
curl -L -o "\${SCRATCH}/Lora-Italic-VariableFont_wght.ttf" \\
|
|
"https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/variable/Lora-Italic%5Bwght%5D.ttf"
|
|
for v in Regular Medium SemiBold Bold; do
|
|
curl -L -o "\${SCRATCH}/IBMPlexSans-\${v}.ttf" \\
|
|
"https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-\${v}.ttf"
|
|
done
|
|
for v in Regular Medium; do
|
|
curl -L -o "\${SCRATCH}/IBMPlexMono-\${v}.ttf" \\
|
|
"https://raw.githubusercontent.com/IBM/plex/master/packages/plex-mono/fonts/complete/ttf/IBMPlexMono-\${v}.ttf"
|
|
done
|
|
EOF
|
|
return 2
|
|
fi
|
|
|
|
mkdir -p "${FONTS_OUT_DIR}"
|
|
|
|
echo "==> Subsetting Lora (variable, Cyreal foundry; R2 substitute for Newsreader)"
|
|
subset_face "${scratch}/Lora-VariableFont_wght.ttf" "${FONTS_OUT_DIR}/Lora-VariableFont.woff2"
|
|
subset_face "${scratch}/Lora-Italic-VariableFont_wght.ttf" "${FONTS_OUT_DIR}/Lora-Italic-VariableFont.woff2"
|
|
|
|
echo "==> Subsetting IBM Plex Sans (4 weights)"
|
|
local variant
|
|
for variant in Regular Medium SemiBold Bold; do
|
|
subset_face "${scratch}/IBMPlexSans-${variant}.ttf" "${FONTS_OUT_DIR}/IBMPlexSans-${variant}.woff2"
|
|
done
|
|
|
|
echo "==> Subsetting IBM Plex Mono (2 weights)"
|
|
for variant in Regular Medium; do
|
|
subset_face "${scratch}/IBMPlexMono-${variant}.ttf" "${FONTS_OUT_DIR}/IBMPlexMono-${variant}.woff2"
|
|
done
|
|
|
|
echo
|
|
echo "==> Bundle summary:"
|
|
du -ch "${FONTS_OUT_DIR}"/*.woff2 | tail -1
|
|
echo
|
|
echo "==> Done. Files emitted under ${FONTS_OUT_DIR}."
|
|
}
|
|
|
|
main "$@"
|