UNPKG

react-fontpicker-ts-lite

Version:

A Google font picker component for React

1 lines 98.9 kB
{"version":3,"sources":["../font-preview/font-previews.css","../src/components/FontPreviews.tsx","../src/components/FontPicker.tsx","../font-preview/fontInfo.json","../src/lib/fontChecker.ts","../src/lib/util.ts"],"sourcesContent":["[class*=\" font-preview-\"],\n[class^=\"font-preview-\"] {\n background-size: 30em auto;\n background-repeat: no-repeat;\n height: 2em;\n image-rendering: optimizequality;\n}\n.font-preview-abril_fatface,\n.font-preview-alegreya,\n.font-preview-alegreya_sans,\n.font-preview-archivo_narrow,\n.font-preview-azeret_mono,\n.font-preview-bad_script,\n.font-preview-biorhyme,\n.font-preview-bitter,\n.font-preview-cardo,\n.font-preview-chivo,\n.font-preview-comic_neue,\n.font-preview-cormorant,\n.font-preview-dancing_script,\n.font-preview-dm_sans,\n.font-preview-dm_serif_display,\n.font-preview-on-all {\n background-image: url(sprite.1.svg);\n}\n.font-preview-dm_serif_text,\n.font-preview-eczar,\n.font-preview-familjen_grotesk,\n.font-preview-fira_sans,\n.font-preview-fraunces,\n.font-preview-ibm_plex_sans,\n.font-preview-inconsolata,\n.font-preview-inknut_antiqua,\n.font-preview-inter,\n.font-preview-josefin_sans,\n.font-preview-josefin_slab,\n.font-preview-karla,\n.font-preview-lato,\n.font-preview-libre_baskerville,\n.font-preview-libre_franklin,\n.font-preview-on-all {\n background-image: url(sprite.2.svg);\n}\n.font-preview-literata,\n.font-preview-lora,\n.font-preview-manrope,\n.font-preview-merriweather,\n.font-preview-montserrat,\n.font-preview-mukta,\n.font-preview-neuton,\n.font-preview-new_rocker,\n.font-preview-newsreader,\n.font-preview-noto_sans_jp,\n.font-preview-nunito,\n.font-preview-nunito_sans,\n.font-preview-old_standard_tt,\n.font-preview-open_sans,\n.font-preview-oswald,\n.font-preview-on-all {\n background-image: url(sprite.3.svg);\n}\n.font-preview-outfit,\n.font-preview-overpass,\n.font-preview-playfair_display,\n.font-preview-plus_jakarta_sans,\n.font-preview-poppins,\n.font-preview-proza_libre,\n.font-preview-pt_sans,\n.font-preview-pt_serif,\n.font-preview-rajdhani,\n.font-preview-raleway,\n.font-preview-roboto,\n.font-preview-roboto_condensed,\n.font-preview-roboto_mono,\n.font-preview-roboto_serif,\n.font-preview-roboto_slab,\n.font-preview-on-all {\n background-image: url(sprite.4.svg);\n}\n.font-preview-rubik,\n.font-preview-sacramento,\n.font-preview-source_sans_3,\n.font-preview-source_serif_4,\n.font-preview-space_grotesk,\n.font-preview-space_mono,\n.font-preview-spectral,\n.font-preview-syne,\n.font-preview-tangerine,\n.font-preview-ubuntu,\n.font-preview-ultra,\n.font-preview-unifrakturmaguntia,\n.font-preview-varela_round,\n.font-preview-work_sans,\n.font-preview-zilla_slab,\n.font-preview-on-all {\n background-image: url(sprite.5.svg);\n}\n.font-preview-abril_fatface{ background-position: 0px -0em }\n.font-preview-alegreya{ background-position: 0px -2em }\n.font-preview-alegreya_sans{ background-position: 0px -4em }\n.font-preview-archivo_narrow{ background-position: 0px -6em }\n.font-preview-azeret_mono{ background-position: 0px -8em }\n.font-preview-bad_script{ background-position: 0px -10em }\n.font-preview-biorhyme{ background-position: 0px -12em }\n.font-preview-bitter{ background-position: 0px -14em }\n.font-preview-cardo{ background-position: 0px -16em }\n.font-preview-chivo{ background-position: 0px -18em }\n.font-preview-comic_neue{ background-position: 0px -20em }\n.font-preview-cormorant{ background-position: 0px -22em }\n.font-preview-dancing_script{ background-position: 0px -24em }\n.font-preview-dm_sans{ background-position: 0px -26em }\n.font-preview-dm_serif_display{ background-position: 0px -28em }\n.font-preview-dm_serif_text{ background-position: 0px -0em }\n.font-preview-eczar{ background-position: 0px -2em }\n.font-preview-familjen_grotesk{ background-position: 0px -4em }\n.font-preview-fira_sans{ background-position: 0px -6em }\n.font-preview-fraunces{ background-position: 0px -8em }\n.font-preview-ibm_plex_sans{ background-position: 0px -10em }\n.font-preview-inconsolata{ background-position: 0px -12em }\n.font-preview-inknut_antiqua{ background-position: 0px -14em }\n.font-preview-inter{ background-position: 0px -16em }\n.font-preview-josefin_sans{ background-position: 0px -18em }\n.font-preview-josefin_slab{ background-position: 0px -20em }\n.font-preview-karla{ background-position: 0px -22em }\n.font-preview-lato{ background-position: 0px -24em }\n.font-preview-libre_baskerville{ background-position: 0px -26em }\n.font-preview-libre_franklin{ background-position: 0px -28em }\n.font-preview-literata{ background-position: 0px -0em }\n.font-preview-lora{ background-position: 0px -2em }\n.font-preview-manrope{ background-position: 0px -4em }\n.font-preview-merriweather{ background-position: 0px -6em }\n.font-preview-montserrat{ background-position: 0px -8em }\n.font-preview-mukta{ background-position: 0px -10em }\n.font-preview-neuton{ background-position: 0px -12em }\n.font-preview-new_rocker{ background-position: 0px -14em }\n.font-preview-newsreader{ background-position: 0px -16em }\n.font-preview-noto_sans_jp{ background-position: 0px -18em }\n.font-preview-nunito{ background-position: 0px -20em }\n.font-preview-nunito_sans{ background-position: 0px -22em }\n.font-preview-old_standard_tt{ background-position: 0px -24em }\n.font-preview-open_sans{ background-position: 0px -26em }\n.font-preview-oswald{ background-position: 0px -28em }\n.font-preview-outfit{ background-position: 0px -0em }\n.font-preview-overpass{ background-position: 0px -2em }\n.font-preview-playfair_display{ background-position: 0px -4em }\n.font-preview-plus_jakarta_sans{ background-position: 0px -6em }\n.font-preview-poppins{ background-position: 0px -8em }\n.font-preview-proza_libre{ background-position: 0px -10em }\n.font-preview-pt_sans{ background-position: 0px -12em }\n.font-preview-pt_serif{ background-position: 0px -14em }\n.font-preview-rajdhani{ background-position: 0px -16em }\n.font-preview-raleway{ background-position: 0px -18em }\n.font-preview-roboto{ background-position: 0px -20em }\n.font-preview-roboto_condensed{ background-position: 0px -22em }\n.font-preview-roboto_mono{ background-position: 0px -24em }\n.font-preview-roboto_serif{ background-position: 0px -26em }\n.font-preview-roboto_slab{ background-position: 0px -28em }\n.font-preview-rubik{ background-position: 0px -0em }\n.font-preview-sacramento{ background-position: 0px -2em }\n.font-preview-source_sans_3{ background-position: 0px -4em }\n.font-preview-source_serif_4{ background-position: 0px -6em }\n.font-preview-space_grotesk{ background-position: 0px -8em }\n.font-preview-space_mono{ background-position: 0px -10em }\n.font-preview-spectral{ background-position: 0px -12em }\n.font-preview-syne{ background-position: 0px -14em }\n.font-preview-tangerine{ background-position: 0px -16em }\n.font-preview-ubuntu{ background-position: 0px -18em }\n.font-preview-ultra{ background-position: 0px -20em }\n.font-preview-unifrakturmaguntia{ background-position: 0px -22em }\n.font-preview-varela_round{ background-position: 0px -24em }\n.font-preview-work_sans{ background-position: 0px -26em }\n.font-preview-zilla_slab{ background-position: 0px -28em }","import { ReactNode } from 'react'\nimport '../../font-preview/font-previews.css'\n\nconst FontPreviews = ({ children }: { children?: ReactNode }) => <>{children}</>\n\nexport default FontPreviews\n","import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport fontInfos from '../../font-preview/fontInfo.json'\nimport { checkLoaded } from '../lib/fontChecker'\nimport { sanify } from '../lib/util'\nconst FontPreviews = lazy(() => import('./FontPreviews'))\nimport './FontPicker.css'\n\nexport interface FontPickerProps extends React.ComponentPropsWithoutRef<'div'> {\n defaultValue?: string\n noMatches?: string\n autoLoad?: boolean\n loaderOnly?: boolean\n loadAllVariants?: boolean\n loadFonts?: string[] | FontToVariant[] | string\n googleFonts?: string[] | Font[] | string | ((font: Font) => boolean)\n fontCategories?: string[] | string\n localFonts?: Font[] | undefined\n\n mode?: 'combo' | 'list'\n\n // For pairing with form labels\n inputId?: string\n\n // Callbacks to emit selected font\n fontVariants?: (fontVariants: FontToVariant) => void\n value?: (value: string) => void\n\n // To have the fontpicker poll and emit whether fonts have been loaded, if applicable\n fontsLoaded?: (fontsLoaded: boolean) => void\n fontsLoadedTimeout?: number // ms\n\n // Fallback component to display inside picker input while loading preview CSS\n loading?: React.ReactNode\n}\n\nexport interface Font {\n category: string\n name: string\n sane: string\n cased: string\n variants: Variant[]\n isLocal?: boolean\n subsets?: string[]\n}\n\nexport interface FourFonts {\n regular?: number\n bold?: number\n italic?: number\n boldItalic?: number\n}\n\nexport type Variant = FontVariant | string\n\nexport interface FontVariant {\n italic: boolean\n weight: number\n}\n\nexport interface FontToVariant {\n fontName: string\n variants: Variant[]\n}\n\nexport function toString(v: Variant) {\n if (typeof v === 'string') {\n return v\n }\n return (v.italic ? '1' : '0') + ',' + v.weight\n}\n\nconst defaultFont: Font = {\n category: 'sans-serif',\n name: 'Open Sans',\n sane: 'open_sans',\n cased: 'open sans',\n variants: [\n '0,300',\n '0,400',\n '0,500',\n '0,600',\n '0,700',\n '0,800',\n '1,300',\n '1,400',\n '1,500',\n '1,600',\n '1,700',\n '1,800',\n ],\n}\n\nexport default function FontPicker({\n defaultValue = 'Open Sans',\n noMatches = 'No matches',\n autoLoad = false,\n loaderOnly = false,\n loadAllVariants = false,\n loadFonts = '',\n googleFonts = 'all',\n fontCategories = 'all',\n localFonts = [],\n mode = 'combo',\n fontVariants,\n value,\n fontsLoaded,\n fontsLoadedTimeout,\n loading = <div>Font previews loading ...</div>,\n inputId,\n ...rest\n}: FontPickerProps) {\n const [focused, setFocused] = useState(false)\n const [selectedFontIndex, setSelectedFontIndex] = useState(-1)\n const [currentFontIndex, setCurrentFontIndex] = useState(-1)\n const prevLoadFontsRef = useRef<string[]>([])\n const inputRef = useRef<HTMLInputElement>(null)\n const popoutRef = useRef<HTMLDivElement>(null)\n const previewRef = useRef<HTMLDivElement>(null)\n\n const handleLoadFont = () => {\n if (typeof loadFonts === 'string') {\n if (loadFonts !== '') {\n const fontNames = loadFonts.trim().split(',')\n fontNames?.forEach((fontName: string) => {\n if (!prevLoadFontsRef.current.includes(fontName)) {\n prevLoadFontsRef.current.push(fontName)\n loadFontByName(fontName)\n }\n })\n }\n } else {\n loadFonts.forEach((font: FontToVariant | string) => {\n const fontName = typeof font === 'string' ? font : font?.fontName\n if (!fontName) {\n return\n }\n if (!prevLoadFontsRef.current.includes(fontName)) {\n prevLoadFontsRef.current.push(fontName)\n if (typeof font === 'object' && font.variants) {\n loadFontByName(\n fontName,\n font.variants.map((v) => toString(v))\n )\n } else {\n loadFontByName(fontName)\n }\n }\n })\n }\n }\n\n // Map loadFonts, a string or array of fonts or font variants to load specified by user, to just the font names (families).\n const loadFontsFontNames = useMemo(() => {\n const names: string[] = []\n if (loadFonts) {\n if (typeof loadFonts === 'string') {\n names.push(loadFonts)\n } else {\n loadFonts?.forEach((font: FontToVariant | string) => {\n const fontName = typeof font === 'string' ? font : font?.fontName\n if (!fontName) {\n return\n }\n names.push(fontName)\n })\n }\n }\n return names\n }, [loadFonts])\n\n // Dynamic refs to allow immediately updating highlighted state of selected font option in picker\n const fontPickerOptionsRef = useRef<Map<string, HTMLDivElement | null> | null>(null)\n\n const saveOptionsRef = (key: string) => (node: HTMLDivElement | null) => {\n const map = getOptionsRef()\n if (node) {\n map.set(key, node)\n } else {\n map.delete(key)\n }\n }\n\n const getOptionsRef = () => {\n if (!fontPickerOptionsRef.current) {\n fontPickerOptionsRef.current = new Map()\n }\n return fontPickerOptionsRef.current\n }\n\n const outerClasses = () => {\n const ret = ['fontpicker']\n return ret\n }\n\n const popoutClasses = () => {\n const ret = ['fontpicker__popout']\n if (focused) {\n ret.push('fontpicker__active')\n }\n return [...ret]\n }\n\n const previewClasses = () => {\n const ret = ['fontpicker__preview']\n ret.push('font-preview-' + current.sane)\n return [...ret]\n }\n\n const allGoogleFonts: Font[] = useMemo(() => {\n const ifonts: Font[] = []\n fontInfos.forEach((info: Omit<Font, 'cased'>) => {\n const font: Font = { ...info, cased: info.name.toLowerCase() }\n ifonts.push(font)\n })\n return [...ifonts]\n }, [])\n\n const fonts = useMemo(() => {\n let activeFonts: Font[]\n if (googleFonts === 'all') {\n activeFonts = [...allGoogleFonts]\n } else if (typeof googleFonts === 'string') {\n const fontNames = googleFonts\n .trim()\n .split(',')\n .map((v) => v.toLowerCase())\n activeFonts = [...allGoogleFonts.filter((a: Font) => fontNames.includes(a.cased))]\n } else if (typeof googleFonts === 'function') {\n activeFonts = [...allGoogleFonts.filter(googleFonts)]\n } else {\n const fontNames = googleFonts?.map((v) => {\n if (typeof v === 'string') {\n return v.toLowerCase()\n } else {\n return v.cased\n }\n }) ?? []\n activeFonts = [...allGoogleFonts.filter((a: Font) => fontNames.includes(a.cased))]\n }\n localFonts.forEach((font: Font) => {\n activeFonts.push({\n category: font.category,\n name: font.name,\n cased: font.name.toLowerCase(),\n sane: sanify(font.name),\n variants: font.variants.map((v: Variant) => toString(v)),\n isLocal: true,\n })\n })\n let activeFontsInCategory: Font[]\n if (fontCategories === 'all') {\n activeFontsInCategory = [...activeFonts]\n } else if (typeof fontCategories === 'string') {\n const newFontCategories: string[] = fontCategories\n .trim()\n .split(',')\n .map((v: string) => v.trim().toLowerCase())\n activeFontsInCategory = [...allGoogleFonts.filter((a: Font) => newFontCategories.includes(a.category))]\n } else {\n const newFontCategories = fontCategories.map((v) => v.toLowerCase())\n activeFontsInCategory = [...allGoogleFonts.filter((a: Font) => newFontCategories.includes(a.category))]\n }\n return [...activeFontsInCategory]\n }, [googleFonts, allGoogleFonts, localFonts, fontCategories])\n\n // Corrected from default font (\"Open Sans\") if default doesn't exist in currently allowed (filtered) fonts\n const saneDefaultValue = useMemo(() => {\n const search = defaultValue.toLowerCase().trim()\n if (!fonts || fonts?.length <= 0) {\n return defaultValue\n }\n if (fonts.some((a) => a.cased === search)) {\n return defaultValue\n }\n return fonts[0].name\n }, [fonts, defaultValue])\n\n const [typedSearch, setTypedSearch] = useState(saneDefaultValue)\n const [searchContent, setSearchContent] = useState(saneDefaultValue)\n\n const matchingFonts = useMemo(() => {\n const search = typedSearch.toLowerCase().trim()\n return fonts.filter((a) => a.cased.includes(search))\n }, [typedSearch, fonts])\n\n const cancelBlur = (e: React.MouseEvent<HTMLDivElement>) => {\n e.preventDefault()\n }\n\n const searchChanged = (e: React.FormEvent<HTMLInputElement>) => {\n if (popoutRef?.current?.scrollTop) {\n popoutRef.current.scrollTop = 0\n }\n const newValue = e.currentTarget?.value\n setSelectedFontIndex(-1)\n const isLonger = typedSearch.length < newValue?.length\n setTypedSearch(newValue)\n\n if (!isLonger) {\n //Don't autocomplete when using backspace\n setSearchContent(newValue)\n return\n }\n\n const cased = newValue.toLowerCase()\n\n const matches = fonts.filter((a) => a.cased.startsWith(cased))\n if (matches?.length) {\n const firstMatch = matches[0].name\n if (e.currentTarget) {\n e.currentTarget.value = firstMatch\n }\n setInputSelection(e.currentTarget, newValue.length, firstMatch.length)\n setSearchContent(firstMatch)\n } else {\n setSearchContent(newValue)\n }\n }\n\n const setInputSelection = (input: EventTarget & HTMLInputElement, startPos: number, endPos: number) => {\n if (input.setSelectionRange) {\n input.setSelectionRange(startPos, endPos)\n }\n }\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key && e.key === 'Enter') {\n const cased = searchContent.toLowerCase()\n const preciseMatches = fonts.filter((a) => a.cased === cased)\n if (selectedFontIndex > -1) {\n setCurrent(matchingFonts[selectedFontIndex])\n } else if (preciseMatches.length === 1) {\n setCurrent(preciseMatches[0])\n } else if (matchingFonts.length > 0) {\n setCurrent(matchingFonts[0])\n } else {\n setCurrent(current)\n }\n } else if (e.key && e.key === 'ArrowDown') {\n e.preventDefault()\n if (selectedFontIndex < matchingFonts.length - 1) {\n setSearchContent(typedSearch)\n const prevSelectedFontIndex = selectedFontIndex\n setSelectedFontIndex((prev) => prev + 1)\n showSelectedFont('key', prevSelectedFontIndex + 1)\n }\n } else if (e.key && e.key === 'ArrowUp') {\n e.preventDefault()\n if (selectedFontIndex > 0) {\n setSearchContent(typedSearch)\n const prevSelectedFontIndex = selectedFontIndex\n setSelectedFontIndex((prev) => prev - 1)\n showSelectedFont('key', prevSelectedFontIndex - 1)\n }\n } else if (e.key && e.key === 'Escape') {\n hide()\n }\n }\n\n const onKeyDownList = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key && e.key === 'Enter') {\n if (selectedFontIndex > -1) {\n setCurrent(fonts[selectedFontIndex])\n } else {\n setCurrent(current)\n }\n } else if (e.key && e.key === 'ArrowDown') {\n e.preventDefault()\n if (selectedFontIndex < fonts.length - 1) {\n setSelectedFontIndex((prev) => prev + 1)\n }\n } else if (e.key && e.key === 'ArrowUp') {\n e.preventDefault()\n if (selectedFontIndex > 0) {\n setSelectedFontIndex((prev) => prev - 1)\n }\n }\n }\n\n const showSelectedFont = (why = 'key', index = selectedFontIndex) => {\n const popout = popoutRef?.current\n if (popout && index >= 0) {\n const selectedFont = matchingFonts[index]\n // selectedFont should be undefined IFF fontCategories is changed and the currently\n // selectedFont is not in the new category.\n if (selectedFont) {\n const fontPreviewElement = popout.querySelector('.font-preview-' + selectedFont.sane) as HTMLElement\n const fontOptionElement = fontPreviewElement?.parentElement\n if (fontOptionElement && fontOptionElement instanceof HTMLElement) {\n const fontTop = fontOptionElement.offsetTop\n const fontBottom = fontTop + fontOptionElement.offsetHeight\n const popTop = popout.scrollTop\n const popBottom = popTop + popout.clientHeight\n if (why === 'opening' || fontTop <= popTop) {\n popout.scrollTop = fontTop\n fontOptionElement.classList.add('selected')\n const optionRef = getOptionsRef()?.get(selectedFont.sane)\n optionRef?.classList.add('selected')\n } else if (fontBottom >= popBottom) {\n popout.scrollTop = fontBottom - popout.clientHeight - 1\n }\n }\n }\n }\n }\n\n const onFocus = () => {\n inputRef?.current?.select()\n setTypedSearch('')\n show()\n }\n\n const onClick = (font: Font) => {\n setCurrent(font)\n hide()\n }\n\n const show = () => {\n if (!focused) {\n setFocused(true)\n setTimeout(() => {\n let newSelectedFontIndex = selectedFontIndex\n for (const [i, font] of matchingFonts.entries()) {\n if (font.name === current.name) {\n newSelectedFontIndex = i\n setSelectedFontIndex(i)\n break\n }\n }\n setSearchContent(current.name)\n showSelectedFont('opening', newSelectedFontIndex)\n }, 1)\n }\n }\n\n const hide = () => {\n inputRef?.current?.blur()\n setFocused(false)\n }\n\n const getFontByName = useCallback(\n (name: string) => {\n let found: Font | null = null\n fonts.forEach((font) => {\n if (font.name === name.trim()) {\n found = font\n }\n })\n return found\n },\n [fonts]\n )\n\n const setCurrent = (font: Font) => {\n setCurrentState(font)\n setTypedSearch(font.name)\n setSearchContent(font.name)\n inputRef?.current?.blur()\n autoLoadFont(font)\n emitFontVariants(font)\n emitValue(font)\n setCurrentFontIndex(fontIndex(font))\n }\n\n const fontIndex = useCallback(\n (font: Font) => {\n let idx\n if (mode === 'list') {\n idx = fonts?.findIndex(f => f.name === font.name) ?? -1\n } else {\n idx = matchingFonts?.findIndex(f => f.name === font.name) ?? -1\n }\n if (idx < 0) {\n idx = 0\n }\n return idx\n }, [fonts, matchingFonts, mode]\n )\n\n const emitFontVariants = useCallback(\n (font: Font) => {\n if (font?.name && font?.variants) {\n fontVariants?.({\n fontName: font.name,\n variants: font.variants,\n })\n }\n },\n [fontVariants]\n )\n\n const emitValue = useCallback(\n (font: Font) => {\n if (font?.name) {\n value?.(font.name)\n }\n },\n [value]\n )\n\n const emitFontLoaded = useCallback(\n (isLoaded: boolean) => {\n fontsLoaded?.(isLoaded)\n },\n [fontsLoaded]\n )\n\n const loadFontFromObject = useCallback(\n (font: Font, variants: Variant[] = []) => {\n if (font?.isLocal) {\n // Don't try to load manually added fonts\n return\n }\n if (variants?.length > 0) {\n variants = font.variants.filter((v: Variant) => variants.includes(v))\n } else if (loadAllVariants) {\n variants = font.variants\n } else {\n variants = getFourVariants(font.variants.map((v) => toString(v)))\n }\n\n let cssId = 'google-font-' + font.sane\n const cssIdAll = cssId + '-all'\n if (variants.length === font.variants.length) {\n cssId = cssIdAll\n } else {\n cssId += '-' + variants.sort().join('-').replaceAll('1,', 'i').replaceAll('0,', '')\n }\n\n const existing = document.getElementById(cssId)\n const existingAll = document.getElementById(cssIdAll)\n if (!existing && !existingAll && font?.name && variants?.length > 0) {\n const link = document.createElement('link')\n link.rel = 'stylesheet'\n link.id = cssId\n link.href =\n 'https://fonts.googleapis.com/css2?family=' +\n font.name +\n ':ital,wght@' +\n variants.sort().join(';') +\n '&display=swap'\n link.setAttribute('data-testid', cssId) // for react testing library\n document.head.appendChild(link)\n }\n },\n [loadAllVariants]\n )\n\n const autoLoadFont = useCallback(\n (font: Font) => {\n if (autoLoad) {\n loadFontFromObject(font)\n }\n },\n [autoLoad, loadFontFromObject]\n )\n\n const loadFontByName = (font: string | Font, variants: Variant[] = []) => {\n if (font === '') {\n return\n }\n let loaded: string | Font | null = font\n if (typeof font === 'string') {\n loaded = getFontByName(font)\n }\n if (loaded === null || typeof loaded !== 'object' || typeof loaded.sane !== 'string') {\n console.error('Unknown font', font)\n } else if (loaded.variants.length < 1) {\n console.error('No valid variants of font', variants)\n } else {\n loadFontFromObject(loaded, variants)\n }\n }\n\n const getFourVariants = (variants: string[]) => {\n const regularWeights = variants\n .filter((v: string) => v.substring(0, 2) === '0,')\n .map((v: string) => parseInt(v.substring(2)))\n .sort((a, b) => a - b)\n const italicWeights = variants\n .filter((v: string) => v.substring(0, 2) === '1,')\n .map((v: string) => parseInt(v.substring(2)))\n .sort((a, b) => a - b)\n\n const fourFonts: FourFonts = {}\n\n // Best regular font is whatever is closest to 400 (but use 300 if only 300 and 500 available)\n fourFonts.regular = regularWeights.sort((a, b) => Math.abs(399 - a) - Math.abs(399 - b)).shift()\n\n // Best bold font is whatever is larger than regular, and closest to 700\n fourFonts.bold = regularWeights\n .filter((v) => v > (fourFonts.regular || 0))\n .sort((a, b) => Math.abs(700 - a) - Math.abs(700 - b))\n .shift()\n\n // Same for italics\n fourFonts.italic = italicWeights.sort((a, b) => Math.abs(399 - a) - Math.abs(399 - b)).shift()\n fourFonts.boldItalic = italicWeights\n .filter((v) => v > (fourFonts.italic || 0))\n .sort((a, b) => Math.abs(700 - a) - Math.abs(700 - b))\n .shift()\n\n const fourVariants: string[] = []\n if (fourFonts.regular) {\n fourVariants.push('0,' + fourFonts.regular)\n }\n if (fourFonts.bold) {\n fourVariants.push('0,' + fourFonts.bold)\n }\n if (fourFonts.italic) {\n fourVariants.push('1,' + fourFonts.italic)\n }\n if (fourFonts.boldItalic) {\n fourVariants.push('1,' + fourFonts.boldItalic)\n }\n return fourVariants\n }\n\n const defaultCurrent = getFontByName(saneDefaultValue) || defaultFont\n const [current, setCurrentState] = useState<Font>(defaultCurrent)\n\n handleLoadFont()\n\n useEffect(() => {\n setCurrent(defaultCurrent)\n }, [defaultCurrent])\n\n useEffect(() => {\n // Check if the currently selected font family and all font families in loadFonts are loaded.\n // Note: Ignores font variants specified by loadFonts! To check all variants themselves are loaded: need to recreate four variants\n // if loadAllVariants not specified, and then parse variant strings into font styles and weights, and then check each variant.\n async function callCheckLoaded(fontNames: string[]) {\n try {\n const promises = fontNames?.map(async (font): Promise<boolean> => {\n try {\n const fontLoaded = await checkLoaded({\n fontFamily: font,\n timeout: fontsLoadedTimeout,\n })\n return fontLoaded\n } catch (e) {\n return false\n }\n })\n const results = await Promise.all(promises)\n // If any of the results are false, not all fonts are loaded\n const allLoaded = !results.some((res) => !res)\n emitFontLoaded(allLoaded)\n } catch (e) {\n console.error(`Exception thrown checking if font families loaded`)\n console.error(e)\n emitFontLoaded(false)\n }\n }\n const fontsToCheck: string[] = [...new Set([current.name, ...loadFontsFontNames])]\n if (fontsLoaded) {\n callCheckLoaded(fontsToCheck)\n }\n }, [autoLoad, loadFonts, current.name, emitFontLoaded, fontsLoaded, loadFontsFontNames, fontsLoadedTimeout])\n\n const renderFontOption = (font: Font, i: number) => {\n return (\n <div\n ref={saveOptionsRef(font.sane)}\n key={font.sane + i}\n className={\n 'fontpicker__option' +\n (i === selectedFontIndex ? ' selected' : '') +\n (i === currentFontIndex ? ' current' : '')\n }\n onMouseDown={() => onClick(font)}\n onMouseMove={() => {\n setSelectedFontIndex(i)\n }}\n role=\"option\"\n >\n <div className={'font-preview-' + font.sane} />\n </div>\n )\n }\n\n return (\n <>\n {!loaderOnly && (\n <div className={outerClasses()?.join(' ')} {...rest}>\n <Suspense fallback={<>{loading}</>}>\n <FontPreviews />\n {mode === 'list' ? (\n <div\n className=\"fontpicker__listbox\"\n role=\"listbox\"\n tabIndex={0}\n onKeyDown={onKeyDownList}\n >\n {fonts.map((font, i) => renderFontOption(font, i))}\n </div>\n ) : (\n <>\n <div ref={previewRef} className={previewClasses()?.join(' ')} />\n <input\n id={inputId}\n className={'fontpicker__search'}\n ref={inputRef}\n type=\"text\"\n onInput={searchChanged}\n onFocus={onFocus}\n onBlur={hide}\n onKeyDown={onKeyDown}\n value={searchContent}\n />\n <div //\n ref={popoutRef}\n tabIndex={-1}\n className={popoutClasses()?.join(' ')}\n onMouseDown={cancelBlur}\n role=\"listbox\"\n >\n {matchingFonts.map((font, i) => renderFontOption(font, i))}\n {matchingFonts.length === 0 && <div className={'fontpicker__nomatches'}>{noMatches}</div>}\n </div>\n </>\n )}\n </Suspense>\n </div>\n )}\n </>\n )\n}\n","[\n {\n \"category\": \"display\",\n \"name\": \"Abril Fatface\",\n \"sane\": \"abril_fatface\",\n \"variants\": [\n \"0,400\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Alegreya\",\n \"sane\": \"alegreya\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Alegreya Sans\",\n \"sane\": \"alegreya_sans\",\n \"variants\": [\n \"0,100\",\n \"1,100\",\n \"0,300\",\n \"1,300\",\n \"0,400\",\n \"1,400\",\n \"0,500\",\n \"1,500\",\n \"0,700\",\n \"1,700\",\n \"0,800\",\n \"1,800\",\n \"0,900\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Archivo Narrow\",\n \"sane\": \"archivo_narrow\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"monospace\",\n \"name\": \"Azeret Mono\",\n \"sane\": \"azeret_mono\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"handwriting\",\n \"name\": \"Bad Script\",\n \"sane\": \"bad_script\",\n \"variants\": [\n \"0,400\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"BioRhyme\",\n \"sane\": \"biorhyme\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Bitter\",\n \"sane\": \"bitter\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Cardo\",\n \"sane\": \"cardo\",\n \"variants\": [\n \"0,400\",\n \"1,400\",\n \"0,700\"\n ],\n \"subsets\": [\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Chivo\",\n \"sane\": \"chivo\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"handwriting\",\n \"name\": \"Comic Neue\",\n \"sane\": \"comic_neue\",\n \"variants\": [\n \"0,300\",\n \"1,300\",\n \"0,400\",\n \"1,400\",\n \"0,700\",\n \"1,700\"\n ],\n \"subsets\": [\n \"latin\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Cormorant\",\n \"sane\": \"cormorant\",\n \"variants\": [\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"handwriting\",\n \"name\": \"Dancing Script\",\n \"sane\": \"dancing_script\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"DM Sans\",\n \"sane\": \"dm_sans\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"DM Serif Display\",\n \"sane\": \"dm_serif_display\",\n \"variants\": [\n \"0,400\",\n \"1,400\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"DM Serif Text\",\n \"sane\": \"dm_serif_text\",\n \"variants\": [\n \"0,400\",\n \"1,400\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Eczar\",\n \"sane\": \"eczar\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\"\n ],\n \"subsets\": [\n \"devanagari\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Familjen Grotesk\",\n \"sane\": \"familjen_grotesk\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Fira Sans\",\n \"sane\": \"fira_sans\",\n \"variants\": [\n \"0,100\",\n \"1,100\",\n \"0,200\",\n \"1,200\",\n \"0,300\",\n \"1,300\",\n \"0,400\",\n \"1,400\",\n \"0,500\",\n \"1,500\",\n \"0,600\",\n \"1,600\",\n \"0,700\",\n \"1,700\",\n \"0,800\",\n \"1,800\",\n \"0,900\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Fraunces\",\n \"sane\": \"fraunces\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"IBM Plex Sans\",\n \"sane\": \"ibm_plex_sans\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"monospace\",\n \"name\": \"Inconsolata\",\n \"sane\": \"inconsolata\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Inknut Antiqua\",\n \"sane\": \"inknut_antiqua\",\n \"variants\": [\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\"\n ],\n \"subsets\": [\n \"devanagari\",\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Inter\",\n \"sane\": \"inter\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Josefin Sans\",\n \"sane\": \"josefin_sans\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Josefin Slab\",\n \"sane\": \"josefin_slab\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"latin\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Karla\",\n \"sane\": \"karla\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Lato\",\n \"sane\": \"lato\",\n \"variants\": [\n \"0,100\",\n \"1,100\",\n \"0,300\",\n \"1,300\",\n \"0,400\",\n \"1,400\",\n \"0,700\",\n \"1,700\",\n \"0,900\",\n \"1,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Libre Baskerville\",\n \"sane\": \"libre_baskerville\",\n \"variants\": [\n \"0,400\",\n \"1,400\",\n \"0,700\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Libre Franklin\",\n \"sane\": \"libre_franklin\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Literata\",\n \"sane\": \"literata\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Lora\",\n \"sane\": \"lora\",\n \"variants\": [\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"math\",\n \"symbols\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Manrope\",\n \"sane\": \"manrope\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Merriweather\",\n \"sane\": \"merriweather\",\n \"variants\": [\n \"0,300\",\n \"1,300\",\n \"0,400\",\n \"1,400\",\n \"0,700\",\n \"1,700\",\n \"0,900\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Montserrat\",\n \"sane\": \"montserrat\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,100\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Mukta\",\n \"sane\": \"mukta\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\"\n ],\n \"subsets\": [\n \"devanagari\",\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Neuton\",\n \"sane\": \"neuton\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"1,400\",\n \"0,700\",\n \"0,800\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"display\",\n \"name\": \"New Rocker\",\n \"sane\": \"new_rocker\",\n \"variants\": [\n \"0,400\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Newsreader\",\n \"sane\": \"newsreader\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Noto Sans JP\",\n \"sane\": \"noto_sans_jp\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"japanese\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Nunito\",\n \"sane\": \"nunito\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Nunito Sans\",\n \"sane\": \"nunito_sans\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\",\n \"1,200\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\",\n \"1,900\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"serif\",\n \"name\": \"Old Standard TT\",\n \"sane\": \"old_standard_tt\",\n \"variants\": [\n \"0,400\",\n \"1,400\",\n \"0,700\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Open Sans\",\n \"sane\": \"open_sans\",\n \"variants\": [\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"1,300\",\n \"1,400\",\n \"1,500\",\n \"1,600\",\n \"1,700\",\n \"1,800\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"greek\",\n \"greek-ext\",\n \"hebrew\",\n \"latin\",\n \"latin-ext\",\n \"math\",\n \"symbols\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Oswald\",\n \"sane\": \"oswald\",\n \"variants\": [\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\"\n ],\n \"subsets\": [\n \"cyrillic\",\n \"cyrillic-ext\",\n \"latin\",\n \"latin-ext\",\n \"vietnamese\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Outfit\",\n \"sane\": \"outfit\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\n \"0,500\",\n \"0,600\",\n \"0,700\",\n \"0,800\",\n \"0,900\"\n ],\n \"subsets\": [\n \"latin\",\n \"latin-ext\"\n ]\n },\n {\n \"category\": \"sans-serif\",\n \"name\": \"Overpass\",\n \"sane\": \"overpass\",\n \"variants\": [\n \"0,100\",\n \"0,200\",\n \"0,300\",\n \"0,400\",\