| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,11 @@ const props = defineProps<{ | |||
| 11 | 11 | ||
| 12 | 12 | const { $highlightCode } = useNuxtApp() | |
| 13 | 13 | ||
| 14 | + // MicroLighter matches its grammar synchronously, and the cost is not linear in the length of the | ||
| 15 | + // block: 60 kB of resolved CSS locks the main thread for minutes. Past this it renders plain. | ||
| 16 | + const HIGHLIGHT_LIMIT = 16_000 | ||
| 17 | + const highlighted = computed(() => props.language && props.code.length <= HIGHLIGHT_LIMIT ? props.language : undefined) | ||
| 18 | + | ||
| 14 | 19 | // The highlighter walks the DOM once per navigation, so a block whose contents change in place | |
| 15 | 20 | // has to ask for another pass. | |
| 16 | 21 | watch(() => [props.code, props.language], () => { | |
@@ -111,7 +116,7 @@ onBeforeUnmount(() => clearTimeout(timer)) | |||
| 111 | 116 | ref="pre" | |
| 112 | 117 | class="block__pre" | |
| 113 | 118 | :tabindex="overflowing ? 0 : undefined" | |
| 114 | - ><code :class="language ? `language-${language}` : undefined">{{ code }}</code></pre> | ||
| 119 | + ><code :class="highlighted ? `language-${highlighted}` : undefined">{{ code }}</code></pre> | ||
| 115 | 120 | </div> | |
| 116 | 121 | </template> | |
| 117 | 122 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,21 +30,35 @@ const { drop } = useFontWarmup() | |||
| 30 | 30 | const requested = computed(() => data.value?.requested) | |
| 31 | 31 | const properties = computed(() => data.value?.properties) | |
| 32 | 32 | ||
| 33 | - // The `preview` preset rather than the current selection, so the URL is stable for a family: it | ||
| 34 | - // can be warmed on hover, and narrowing a facet does not drop and re-add every `@font-face`. | ||
| 35 | - const stylesheet = computed(() => { | ||
| 36 | - const scoped = provider.value ? `&provider=${encodeURIComponent(provider.value)}` : '' | ||
| 37 | - return `/api/v1/fonts/${encodeURIComponent(family.value)}/css?preset=preview${scoped}` | ||
| 38 | - }) | ||
| 33 | + const scopedProvider = computed(() => (provider.value ? `&provider=${encodeURIComponent(provider.value)}` : '')) | ||
| 34 | + | ||
| 35 | + // One weight, subset to the specimen's own glyphs, and the URL the grid warms on hover. | ||
| 36 | + const warmStylesheet = computed(() => | ||
| 37 | + `/api/v1/fonts/${encodeURIComponent(family.value)}/css?preset=warm${scopedProvider.value}`, | ||
| 38 | + ) | ||
| 39 | + | ||
| 40 | + /* | ||
| 41 | + * The `preview` preset rather than the current selection, so the URL is stable for a family and | ||
| 42 | + * narrowing a facet does not drop and re-add every `@font-face`. Aliased: declaring it under the | ||
| 43 | + * family name would restyle the specimen to a different file the moment it lands. | ||
| 44 | + */ | ||
| 45 | + const previewFamily = computed(() => `${family.value} preview`) | ||
| 46 | + const stylesheet = computed(() => | ||
| 47 | + `/api/v1/fonts/${encodeURIComponent(family.value)}/css?preset=preview&as=${encodeURIComponent(previewFamily.value)}${scopedProvider.value}`, | ||
| 48 | + ) | ||
| 39 | 49 | ||
| 40 | 50 | useHead(() => ({ | |
| 41 | - link: [{ rel: 'stylesheet', href: stylesheet.value }], | ||
| 51 | + link: [ | ||
| 52 | + { rel: 'stylesheet', href: warmStylesheet.value }, | ||
| 53 | + { rel: 'stylesheet', href: stylesheet.value }, | ||
| 54 | + ], | ||
| 42 | 55 | })) | |
| 43 | 56 | ||
| 44 | - // The warm stylesheet paints the specimen until the preview sheet is both parsed and loaded. | ||
| 45 | - // `document.fonts.ready` alone resolves immediately when no request is yet in flight. | ||
| 57 | + // A hover-warmed sheet declares the same faces, so it is released only once this page's own copy | ||
| 58 | + // is parsed and loaded. `document.fonts.ready` alone resolves immediately when no request is yet | ||
| 59 | + // in flight. | ||
| 46 | 60 | if (import.meta.client) { | |
| 47 | - watch(stylesheet, async (href) => { | ||
| 61 | + watch(warmStylesheet, async (href) => { | ||
| 48 | 62 | await nextTick() | |
| 49 | 63 | const link = document.head.querySelector<HTMLLinkElement>(`link[rel="stylesheet"][href="${href}"]`) | |
| 50 | 64 | if (link && !link.sheet) { | |
@@ -108,8 +122,17 @@ watch([loadedRange, staticWeights], () => { | |||
| 108 | 122 | } | |
| 109 | 123 | }, { immediate: true }) | |
| 110 | 124 | ||
| 125 | + // The warm face holds one weight and only the glyphs of the untouched specimen, so an edit moves | ||
| 126 | + // to the preview alias. On events, not on the values, which normalise on load for a family | ||
| 127 | + // without a 400. | ||
| 128 | + const usingPreview = ref(false) | ||
| 129 | + function needsPreviewFace() { | ||
| 130 | + usingPreview.value = true | ||
| 131 | + } | ||
| 132 | + const specimenFamily = computed(() => (usingPreview.value ? previewFamily.value : family.value)) | ||
| 133 | + | ||
| 111 | 134 | const previewStyle = computed(() => ({ | |
| 112 | - fontFamily: `'${family.value}', '${family.value} fallback', var(--font-display)`, | ||
| 135 | + fontFamily: `'${specimenFamily.value}', '${specimenFamily.value} fallback', var(--font-display)`, | ||
| 113 | 136 | fontSize: `${size.value}px`, | |
| 114 | 137 | fontWeight: String(weight.value), | |
| 115 | 138 | fontStyle: italic.value ? 'italic' : 'normal', | |
@@ -156,6 +179,7 @@ function setProvider(name: string) { | |||
| 156 | 179 | if (name === selectedProvider.value) { | |
| 157 | 180 | return | |
| 158 | 181 | } | |
| 182 | + needsPreviewFace() | ||
| 159 | 183 | router.replace({ | |
| 160 | 184 | query: { | |
| 161 | 185 | ...route.query, | |
@@ -168,6 +192,7 @@ function setProvider(name: string) { | |||
| 168 | 192 | } | |
| 169 | 193 | ||
| 170 | 194 | function apply(key: 'weights' | 'subsets' | 'styles', values: string[]) { | |
| 195 | + needsPreviewFace() | ||
| 171 | 196 | router.replace({ | |
| 172 | 197 | query: { ...route.query, [key]: values.length ? values.join(',') : undefined }, | |
| 173 | 198 | }) | |
@@ -251,12 +276,35 @@ const kb = (bytes: number) => `${(bytes / 1024).toFixed(1)} kB` | |||
| 251 | 276 | /* ── Coverage ───────────────────────────────────────────── */ | |
| 252 | 277 | interface CoverageCheck { text: string, unrestricted: boolean, covered: string[], missing: string[], subsets: string[] } | |
| 253 | 278 | type CoverageRow = Pick<CoverageCheck, 'text' | 'unrestricted' | 'missing'> | |
| 279 | + const coverageSubsets = computed(() => requested.value?.subsets.join(',') ?? '') | ||
| 280 | + | ||
| 281 | + // Aliased away from the specimen family, so a subset the selection dropped cannot paint the row. | ||
| 282 | + const coverageFamily = computed(() => `${family.value} coverage`) | ||
| 283 | + const coverageStylesheet = computed(() => { | ||
| 284 | + if (!coverageSubsets.value) { | ||
| 285 | + return undefined | ||
| 286 | + } | ||
| 287 | + const params = new URLSearchParams({ | ||
| 288 | + weights: String(sampleWeight.value), | ||
| 289 | + styles: 'normal', | ||
| 290 | + subsets: coverageSubsets.value, | ||
| 291 | + as: coverageFamily.value, | ||
| 292 | + }) | ||
| 293 | + if (provider.value) { | ||
| 294 | + params.set('provider', provider.value) | ||
| 295 | + } | ||
| 296 | + return `/api/v1/fonts/${encodeURIComponent(family.value)}/css?${params}` | ||
| 297 | + }) | ||
| 298 | + | ||
| 299 | + useHead(() => ({ | ||
| 300 | + link: coverageStylesheet.value ? [{ rel: 'stylesheet', href: coverageStylesheet.value }] : [], | ||
| 301 | + })) | ||
| 254 | 302 | const { data: coverage } = await useFetch( | |
| 255 | 303 | () => `/api/v1/fonts/${encodeURIComponent(family.value)}/coverage`, | |
| 256 | 304 | // Server-rendered: on the client the coverage rows land after hydration and shift the page. | |
| 257 | 305 | { | |
| 258 | 306 | key: () => `coverage-${family.value}`, | |
| 259 | - query: { provider }, | ||
| 307 | + query: { provider, subsets: coverageSubsets }, | ||
| 260 | 308 | // Only the gaps are rendered. | |
| 261 | 309 | transform: ({ checks }: { checks: Record<string, CoverageCheck> }): { checks: Record<string, CoverageRow> } => ({ | |
| 262 | 310 | checks: Object.fromEntries( | |
@@ -464,6 +512,7 @@ export default defineNuxtConfig({ | |||
| 464 | 512 | rows="2" | |
| 465 | 513 | spellcheck="false" | |
| 466 | 514 | :style="previewStyle" | |
| 515 | + @input="needsPreviewFace()" | ||
| 467 | 516 | /> | |
| 468 | 517 | </div> | |
| 469 | 518 | ||
@@ -519,6 +568,7 @@ export default defineNuxtConfig({ | |||
| 519 | 568 | :min="loadedRange.min" | |
| 520 | 569 | :max="loadedRange.max" | |
| 521 | 570 | step="1" | |
| 571 | + @input="needsPreviewFace()" | ||
| 522 | 572 | > | |
| 523 | 573 | </p> | |
| 524 | 574 | <p | |
@@ -529,6 +579,7 @@ export default defineNuxtConfig({ | |||
| 529 | 579 | <select | |
| 530 | 580 | id="weight" | |
| 531 | 581 | v-model.number="weight" | |
| 582 | + @change="needsPreviewFace()" | ||
| 532 | 583 | > | |
| 533 | 584 | <option | |
| 534 | 585 | v-for="value in staticWeights" | |
@@ -546,6 +597,7 @@ export default defineNuxtConfig({ | |||
| 546 | 597 | v-model="italic" | |
| 547 | 598 | type="checkbox" | |
| 548 | 599 | :disabled="!italicLoaded" | |
| 600 | + @change="needsPreviewFace()" | ||
| 549 | 601 | > | |
| 550 | 602 | <span | |
| 551 | 603 | v-if="!providerHasItalic" | |
@@ -713,7 +765,7 @@ export default defineNuxtConfig({ | |||
| 713 | 765 | Coverage | |
| 714 | 766 | </h3> | |
| 715 | 767 | <p class="meta__lede"> | |
| 716 | - Checked against the <code>unicode-range</code> of every face the provider returned. | ||
| 768 | + Checked against the <code>unicode-range</code> of every face this selection resolved. | ||
| 717 | 769 | </p> | |
| 718 | 770 | <ul | |
| 719 | 771 | v-if="coverage" | |
@@ -728,7 +780,7 @@ export default defineNuxtConfig({ | |||
| 728 | 780 | <span | |
| 729 | 781 | class="coverage__sample" | |
| 730 | 782 | :style="{ | |
| 731 | - fontFamily: `'${family}', '${family} fallback', var(--font-display)`, | ||
| 783 | + fontFamily: `'${coverageFamily}', '${coverageFamily} fallback', var(--font-display)`, | ||
| 732 | 784 | fontWeight: String(sampleWeight), | |
| 733 | 785 | }" | |
| 734 | 786 | >{{ check.text }}</span> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,13 @@ export const SAMPLES: Record<string, string> = { | |||
| 14 | 14 | 'maths': '≈ ≠ ≤ ≥ ± × ÷ ∑ √ ∞', | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | + function list(value: unknown) { | ||
| 18 | + if (typeof value !== 'string' || !value.trim()) { | ||
| 19 | + return undefined | ||
| 20 | + } | ||
| 21 | + return value.split(',').map(part => part.trim()).filter(Boolean) | ||
| 22 | + } | ||
| 23 | + | ||
| 17 | 24 | export default defineEventHandler(async (event) => { | |
| 18 | 25 | const family = decodeURIComponent(getRouterParam(event, 'family') || '') | |
| 19 | 26 | if (!family) { | |
@@ -32,7 +39,9 @@ export default defineEventHandler(async (event) => { | |||
| 32 | 39 | // `unicode-range` does not vary by weight within a family. | |
| 33 | 40 | weights: ['400'], | |
| 34 | 41 | styles: ['normal'], | |
| 35 | - subsets: properties.subsets?.length ? properties.subsets : ['latin'], | ||
| 42 | + // Absent, everything the provider publishes: the answer then describes the family, not a | ||
| 43 | + // selection. | ||
| 44 | + subsets: list(query.subsets) ?? (properties.subsets?.length ? properties.subsets : ['latin']), | ||
| 36 | 45 | formats: ['woff2'], | |
| 37 | 46 | }, allowed) | |
| 38 | 47 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments