Compare commits

..

10 Commits

Author SHA1 Message Date
Rui Saraiva
60d571fe11
chore: update Rui's avatar
Resized from a 512x512 source to 112x112, the size the other two use and
twice the 56px it renders at.

The circle is baked into the file, not applied by CSS — the component sets
no border-radius at all, and the existing avatars are pre-masked with fully
transparent corners. A plain square resize rendered as a square next to two
circles, so the mask is composited in with dest-in.

PNG with palette quantisation: 8.3 KB against 28.6 KB for straight RGBA,
with no difference visible at 3x magnification on either the face or the
masked edge. That also puts it under the 13.1 KB the other two weigh.
2026-07-26 14:49:20 +01:00
Rui Saraiva
135c260189
feat(seo): add a canonical link and a sitemap
favycon.app has no DNS records at all, so favycon.vercel.app is the only
hostname this site answers on and the canonical is unambiguous. It uses the
SITE_URL constant, so it cannot disagree with og:url.

The sitemap is a single static URL. Its loc is written without a trailing
slash to match the canonical string exactly — the two forms are the same
URL per RFC 3986, but nothing here forces them to differ, so they don't.
No lastmod: a hand-maintained file would claim a freshness it can't know.

robots.txt gains the Sitemap line and a note that /api/favycon is POST-only
and answers a GET with 405. It stays out of the disallow list deliberately:
a 405 tells a crawler more than a rule it has to fetch robots.txt to find.
2026-07-26 14:39:13 +01:00
Rui Saraiva
74275a31a7
chore: use the alias address and the current role
The package author email was a Gmail address. The site already contacts
through hello@ruisaraiva.dev, an alias forwarded to that inbox, and the
point of the alias is that a published field can be rotated without
touching the real mailbox — so the package field uses it too.

The credits still described Rui as a Full-Stack Developer, which was true
when the tool was built in 2020. Augusto's and Miguel's roles are left as
they are.
2026-07-26 14:38:40 +01:00
Rui Saraiva
78c9508195
feat(seo): declare the author and add llms.txt
The about modal is lazily mounted, so the credits and contact address are
not in the server-rendered HTML and no crawler ever sees them. Authorship
therefore goes in the head, where it is always present: a meta author, a
rel=author link to ruisaraiva.dev, and SoftwareApplication JSON-LD.

The structured data names Rui as author and Augusto and Miguel as
contributors, because the modal credits three people and the markup should
not tell a machine a tidier story than the page tells a reader.

Also adds public/llms.txt: accepted input formats, the 310px and 512px
minimums, and exactly what the zip contains, including that favicon.svg
only appears for SVG input and manifest.json only with the PWA option. It
is written from utils/favicon.ts and the route handler rather than from
memory. Note it is a static file, so it is the one thing here that can
drift from the sizes it documents.

Two hardcoded favycon.vercel.app strings become the SITE_URL constant the
rest of the file now uses.
2026-07-26 14:31:52 +01:00
Rui Saraiva
3588ab0506
feat: link the author credit to a website and update the contact address
Two changes to the about modal, in the same file:

Rui's credit pointed at a Twitter handle and now points at ruisaraiva.dev,
with a globe icon. Augusto keeps Twitter and Miguel keeps GitHub, so the
link type is now one of three rather than a binary — hence a small lookup
instead of a second ternary in the JSX. The icon is drawn from circle,
line and ellipse primitives rather than path data, so the geometry is
exact.

Because the globe is stroked where the GitHub mark is filled, the
dark-mode rule for it has to set stroke rather than fill. Both were
checked in both themes.

The subtitle said "follow these guys on Twitter", which only one of the
three links now is, so it drops the platform.

The contact address moves from hello@favycon.app, which is no longer in
use, to hello@ruisaraiva.dev.
2026-07-26 14:31:52 +01:00
Rui Saraiva
0705e4e404
chore: upgrade Node and sharp dependency (#484) 2024-11-30 18:17:07 +00:00
Rui Saraiva
a2d70d783f
fix: remove initial background flash 2022-07-07 11:29:08 +01:00
Rui Saraiva
37a1af5f35
Merge pull request #482 from ruisaraiva19/develop 2022-07-07 08:38:55 +01:00
Rui Saraiva
04a34e04c2
chore(deps): upgrade dependencies 2022-07-07 08:26:00 +01:00
Rui Saraiva
a82367ed0c
fix: update security headers 2021-12-12 22:30:29 +00:00
31 changed files with 3797 additions and 3291 deletions

View File

@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Lint commit messages - name: Lint commit messages
uses: wagoid/commitlint-github-action@v2 uses: wagoid/commitlint-github-action@v6

View File

@ -8,9 +8,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'

View File

@ -11,22 +11,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Cache Next.js build cache - name: Cache Next.js build cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ${{ github.workspace }}/.next/cache path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}
- name: Build static assets - name: Build static assets
run: yarn build run: yarn build
- name: Semantic Release - name: Semantic Release
uses: cycjimmy/semantic-release-action@v2 uses: cycjimmy/semantic-release-action@v4
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -7,10 +7,10 @@ jobs:
name: Run Cypress name: Run Cypress
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
@ -19,7 +19,7 @@ jobs:
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Cache Next.js build cache - name: Cache Next.js build cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ${{ github.workspace }}/.next/cache path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}
@ -28,7 +28,7 @@ jobs:
run: yarn build run: yarn build
- name: Run Cypress - name: Run Cypress
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v6
with: with:
start: npm start start: npm start
wait-on: http://localhost:3000 wait-on: http://localhost:3000
@ -36,13 +36,13 @@ jobs:
# store videos and any screenshots # store videos and any screenshots
# NOTE: screenshots will be generated only if E2E test failed # NOTE: screenshots will be generated only if E2E test failed
# thus we store screenshots only on failures # thus we store screenshots only on failures
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: cypress-screenshots name: cypress-screenshots
path: cypress/screenshots path: cypress/screenshots
# Test run video was always captured, so this action uses "always()" condition # Test run video was always captured, so this action uses "always()" condition
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: cypress-videos name: cypress-videos

2
.nvmrc
View File

@ -1 +1 @@
14.18 22.11

View File

@ -1,8 +1,7 @@
{ {
"useTabs": true, "useTabs": true,
"printWidth": 120, "printWidth": 120,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5", "trailingComma": "es5",
"jsxBracketSameLine": true,
"semi": false "semi": false
} }

View File

@ -3,11 +3,11 @@
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"eslint.run": "onType", "eslint.run": "onType",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true, "source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": true "source.fixAll.stylelint": "explicit"
}, },
"css.validate": false, "css.validate": false,
"less.validate": false, "less.validate": false,
"scss.validate": false, "scss.validate": false,
"cssModules.camelCase": true "typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -2,7 +2,7 @@
.base { .base {
padding: 5px 12px; padding: 5px 12px;
font-family: Manrope, Arial, '"Helvetica Neue"', Helvetica, sans-serif; font-family: 'Manrope', Arial, '"Helvetica Neue"', Helvetica, sans-serif;
font-size: $font-size-title; font-size: $font-size-title;
line-height: $line-height-title; line-height: $line-height-title;
color: $black; color: $black;

View File

@ -208,6 +208,7 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
} }
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDrop, onDrop,
accept: Object.keys(ACCEPT_MIME_TYPES), accept: Object.keys(ACCEPT_MIME_TYPES),
maxSize: ONE_MB, maxSize: ONE_MB,
@ -230,13 +231,15 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
className={classnames(styles.container, { className={classnames(styles.container, {
[styles.loading]: isLoading, [styles.loading]: isLoading,
[styles.transparent]: !image, [styles.transparent]: !image,
})}> })}
>
{!image && !zipData && ( {!image && !zipData && (
<> <>
<div <div
className={styles.dropZoneWrapper} className={styles.dropZoneWrapper}
onMouseEnter={() => setIsHover(true)} onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}> onMouseLeave={() => setIsHover(false)}
>
<SvgDropZone className={classnames(styles.dashed, { [styles.dragActive]: isDragActive })} /> <SvgDropZone className={classnames(styles.dashed, { [styles.dragActive]: isDragActive })} />
<div {...getRootProps()} className={styles.dropZone} data-cy="drag-and-drop"> <div {...getRootProps()} className={styles.dropZone} data-cy="drag-and-drop">
<input {...getInputProps()} /> <input {...getInputProps()} />
@ -249,7 +252,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
className={classnames(styles.imageUploadText, styles.mobileUploadText, { className={classnames(styles.imageUploadText, styles.mobileUploadText, {
[styles.dragActive]: isDragActive, [styles.dragActive]: isDragActive,
})} })}
data-cy="drag-and-drop-text"> data-cy="drag-and-drop-text"
>
<span> <span>
Generate all the sizes for your Generate all the sizes for your
<br /> <br />
@ -262,7 +266,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
className={classnames(styles.imageUploadText, styles.desktopUploadText, { className={classnames(styles.imageUploadText, styles.desktopUploadText, {
[styles.dragActive]: isDragActive, [styles.dragActive]: isDragActive,
})} })}
data-cy="drag-and-drop-text"> data-cy="drag-and-drop-text"
>
<CSSTransition in={!isDragActive} timeout={200} classNames="collapse" unmountOnExit> <CSSTransition in={!isDragActive} timeout={200} classNames="collapse" unmountOnExit>
<span>Drag &amp;&nbsp;</span> <span>Drag &amp;&nbsp;</span>
</CSSTransition> </CSSTransition>
@ -313,7 +318,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
weight="semiBold" weight="semiBold"
color="gray" color="gray"
tag="div" tag="div"
className={styles.filenameWrapper}> className={styles.filenameWrapper}
>
<span className={styles.filename} data-cy="preview-filename"> <span className={styles.filename} data-cy="preview-filename">
{imageData.name} {imageData.name}
{imageData.extension} {imageData.extension}
@ -357,7 +363,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
id="pwa" id="pwa"
disabled={PWADisabled} disabled={PWADisabled}
onChange={togglePwa} onChange={togglePwa}
data-cy="preview-pwa-compatible"> data-cy="preview-pwa-compatible"
>
<Typography variant="regularBody" weight="semiBold" muted={PWADisabled}> <Typography variant="regularBody" weight="semiBold" muted={PWADisabled}>
PWA compatible PWA compatible
</Typography> </Typography>
@ -373,7 +380,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
id="dark" id="dark"
disabled disabled
onChange={toggleDarkMode} onChange={toggleDarkMode}
data-cy="preview-dark-mode-version"> data-cy="preview-dark-mode-version"
>
<Typography variant="regularBody" weight="semiBold" muted> <Typography variant="regularBody" weight="semiBold" muted>
Dark Mode version Dark Mode version
</Typography> </Typography>
@ -392,14 +400,18 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
onClick={() => { onClick={() => {
splitbee.track('Re-upload') splitbee.track('Re-upload')
resetImage() resetImage()
}}> }}
>
Re-upload Re-upload
</Button> </Button>
<Button <Button
color="white" color="white"
background="bgLink" background="bgLink"
className={styles.imageGenerate} className={styles.imageGenerate}
onClick={() => generateFavicon(image)}> onClick={() => {
generateFavicon(image)
}}
>
Generate Favicon Generate Favicon
</Button> </Button>
</div> </div>
@ -442,7 +454,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
className={styles.showCode} className={styles.showCode}
variant="regularTransparent" variant="regularTransparent"
color="gray" color="gray"
onClick={() => setIsModalOpen(true)}> onClick={() => setIsModalOpen(true)}
>
Show Show
</Button> </Button>
</div> </div>
@ -455,14 +468,16 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
onClick={() => { onClick={() => {
splitbee.track('Make a new one') splitbee.track('Make a new one')
resetImage() resetImage()
}}> }}
>
Make a new one Make a new one
</Button> </Button>
<Button <Button
color="white" color="white"
background="bgGreen" background="bgGreen"
className={styles.imageGenerate} className={styles.imageGenerate}
onClick={() => onDownload(zipData)}> onClick={() => onDownload(zipData)}
>
Download Favicon Download Favicon
</Button> </Button>
</div> </div>
@ -475,7 +490,8 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
className={styles.content} className={styles.content}
overlayClassName={styles.overlay} overlayClassName={styles.overlay}
closeTimeoutMS={200} closeTimeoutMS={200}
contentLabel="Code generated"> contentLabel="Code generated"
>
<div className={styles.modalContainer} {...bind()}> <div className={styles.modalContainer} {...bind()}>
<div className={classnames(styles.modalHeader, styles.modalHeaderMobile)}> <div className={classnames(styles.modalHeader, styles.modalHeaderMobile)}>
<Typography variant="extraLargeTitle" weight="extraBold" color="white" colorImmutable> <Typography variant="extraLargeTitle" weight="extraBold" color="white" colorImmutable>

View File

@ -38,7 +38,8 @@ const FavyconError = ({ error, clearError }: FavyconErrorProps) => {
variant="regularBody" variant="regularBody"
color="white" color="white"
colorImmutable={true} colorImmutable={true}
weight="semiBold"> weight="semiBold"
>
{error} {error}
</Typography> </Typography>
<svg height={20} width={20}> <svg height={20} width={20}>

View File

@ -290,7 +290,11 @@
margin-right: 2px; margin-right: 2px;
} }
&.personGithub { // Both modifiers do the same job: drop the base class's Twitter blue for the
// neutral ink. Kept as two names rather than one shared class so the markup
// still says which kind of link it is.
&.personGithub,
&.personWebsite {
color: $black; color: $black;
p { p {
@ -316,7 +320,8 @@
} }
} }
:global(.dark) .personTwitter.personGithub p { :global(.dark) .personTwitter.personGithub p,
:global(.dark) .personTwitter.personWebsite p {
color: $white; color: $white;
&:hover { &:hover {
@ -328,6 +333,13 @@
fill: $white; fill: $white;
} }
// The GitHub mark is a filled path; the globe is stroked, so it needs the other
// property. Both are set explicitly rather than relying on currentColor, to
// match how the rule above already works.
:global(.dark) .personTwitter.personWebsite p svg {
stroke: $white;
}
.modalFooter { .modalFooter {
padding-bottom: 48px; padding-bottom: 48px;
margin-top: 12px; margin-top: 12px;

View File

@ -7,6 +7,7 @@ import { Typography } from 'components/typography'
import { ToolTitle } from 'components/tool-title' import { ToolTitle } from 'components/tool-title'
import { SvgTwitter } from 'components/svgs/svg-twitter' import { SvgTwitter } from 'components/svgs/svg-twitter'
import { SvgGithub } from 'components/svgs/svg-github' import { SvgGithub } from 'components/svgs/svg-github'
import { SvgWebsite } from 'components/svgs/svg-website'
import styles from './index.module.scss' import styles from './index.module.scss'
import { SvgFavycon } from 'components/svgs/svg-favycon' import { SvgFavycon } from 'components/svgs/svg-favycon'
@ -19,26 +20,47 @@ import miguelTeixeira from '../../public/images/people/miguel-teixeira.png'
const Modal = dynamic(() => import('react-modal')) const Modal = dynamic(() => import('react-modal'))
// A lookup rather than a chain of ternaries in the JSX, because there are three
// kinds of link now. `personTwitter` is the shared base class — it predates the
// other two and carries the layout, so the modifiers only restate the colour.
const socials = {
twitter: {
Icon: SvgTwitter,
url: (handle: string) => `https://twitter.com/${handle}`,
modifier: undefined,
},
github: {
Icon: SvgGithub,
url: (handle: string) => `https://github.com/${handle}`,
modifier: styles.personGithub,
},
website: {
Icon: SvgWebsite,
url: (handle: string) => `https://${handle}`,
modifier: styles.personWebsite,
},
}
const people = [ const people = [
{ {
screenName: 'aboutaugusto', handle: 'aboutaugusto',
name: 'Augusto Lopes', name: 'Augusto Lopes',
role: 'Product Designer', role: 'Product Designer',
social: 'twitter', social: 'twitter' as const,
photo: augustoLopes, photo: augustoLopes,
}, },
{ {
screenName: 'ruisaraiva19', handle: 'ruisaraiva.dev',
name: 'Rui Saraiva', name: 'Rui Saraiva',
role: 'Full-Stack Developer', role: 'Principal Engineer',
social: 'twitter', social: 'website' as const,
photo: ruiSaraiva, photo: ruiSaraiva,
}, },
{ {
screenName: 'miguellteixeira', handle: 'miguellteixeira',
name: 'Miguel Teixeira', name: 'Miguel Teixeira',
role: 'Full-Stack Developer', role: 'Full-Stack Developer',
social: 'github', social: 'github' as const,
photo: miguelTeixeira, photo: miguelTeixeira,
}, },
] ]
@ -102,7 +124,8 @@ const FavyconInfo = ({
className={styles.content} className={styles.content}
overlayClassName={styles.overlay} overlayClassName={styles.overlay}
closeTimeoutMS={200} closeTimeoutMS={200}
contentLabel="About Us"> contentLabel="About Us"
>
<div className={styles.modalContainer} {...bind()}> <div className={styles.modalContainer} {...bind()}>
<div className={styles.modalHeaderMobile}> <div className={styles.modalHeaderMobile}>
<div className={styles.modalHeaderTop}> <div className={styles.modalHeaderTop}>
@ -137,54 +160,47 @@ const FavyconInfo = ({
Behind the curtains Behind the curtains
</Typography> </Typography>
<Typography variant="largeBody" weight="medium" className={styles.subTitle}> <Typography variant="largeBody" weight="medium" className={styles.subTitle}>
For new updates be sure to follow these guys on Twitter. For new updates be sure to follow these guys.
</Typography> </Typography>
</div> </div>
<hr className={styles.hr} /> <hr className={styles.hr} />
<div className={styles.people}> <div className={styles.people}>
{people.map((person) => ( {people.map((person) => {
<div key={person.screenName} className={styles.person}> const { Icon, url, modifier } = socials[person.social]
<div className={styles.personAvatar}>
<Image return (
src={person.photo} <div key={person.handle} className={styles.person}>
alt={person.name} <div className={styles.personAvatar}>
width={56} <Image
height={56} src={person.photo}
placeholder="blur" alt={person.name}
layout="intrinsic" width={56}
/> height={56}
</div> placeholder="blur"
<div> layout="intrinsic"
<Typography variant="largeBody" weight="semiBold"> />
{person.name} </div>
</Typography> <div>
<Typography variant="regularBody" weight="medium" className={styles.personRole}> <Typography variant="largeBody" weight="semiBold">
{person.role} {person.name}
</Typography> </Typography>
{person.social === 'twitter' ? ( <Typography variant="regularBody" weight="medium" className={styles.personRole}>
{person.role}
</Typography>
<a <a
href={`https://twitter.com/${person.screenName}`} href={url(person.handle)}
className={styles.personTwitter} className={classnames(styles.personTwitter, modifier)}
target="_blank" target="_blank"
rel="noopener noreferrer"> rel="noopener noreferrer"
>
<Typography variant="footer" weight="semiBold"> <Typography variant="footer" weight="semiBold">
<SvgTwitter /> {person.screenName} <Icon /> {person.handle}
</Typography> </Typography>
</a> </a>
) : ( </div>
<a
href={`https://github.com/${person.screenName}`}
className={classnames(styles.personTwitter, styles.personGithub)}
target="_blank"
rel="noopener noreferrer">
<Typography variant="footer" weight="semiBold">
<SvgGithub /> {person.screenName}
</Typography>
</a>
)}
</div> </div>
</div> )
))} })}
</div> </div>
<hr className={styles.hr} /> <hr className={styles.hr} />
<div className={classnames(styles.footer, styles.modalFooter)}> <div className={classnames(styles.footer, styles.modalFooter)}>
@ -194,12 +210,13 @@ const FavyconInfo = ({
className={styles.repo} className={styles.repo}
href="https://github.com/ruisaraiva19/favycon" href="https://github.com/ruisaraiva19/favycon"
target="_blank" target="_blank"
rel="noopener noreferrer"> rel="noopener noreferrer"
>
GitHub GitHub
</a> </a>
. Have feedback? Send it to{' '} . Have feedback? Send it to{' '}
<a className={styles.contact} href="mailto:hello@favycon.app"> <a className={styles.contact} href="mailto:hello@ruisaraiva.dev">
hello@favycon.app hello@ruisaraiva.dev
</a> </a>
</Typography> </Typography>
</div> </div>

View File

@ -1,43 +1,80 @@
import Head from 'next/head' import Head from 'next/head'
const SITE_URL = 'https://favycon.vercel.app'
const AUTHOR = { name: 'Rui Saraiva', url: 'https://ruisaraiva.dev' }
// The tool credits three people in its About modal, so the structured data says
// the same thing rather than a tidier untruth: Rui as author, the other two as
// contributors, linked where the modal links them.
const CONTRIBUTORS = [
{ name: 'Augusto Lopes', url: 'https://twitter.com/aboutaugusto' },
{ name: 'Miguel Teixeira', url: 'https://github.com/miguellteixeira' },
]
type SEOProps = { type SEOProps = {
title: string title: string
description: string description: string
} }
const SEO = ({ title, description }: SEOProps) => ( const SEO = ({ title, description }: SEOProps) => {
<Head> const structuredData = {
<title>{title}</title> '@context': 'https://schema.org',
<meta name="description" content={description} /> '@type': 'SoftwareApplication',
<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png" /> name: 'Favycon',
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png" /> description,
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png" /> url: SITE_URL,
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png" /> applicationCategory: 'DeveloperApplication',
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png" /> operatingSystem: 'Any',
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png" /> browserRequirements: 'Requires JavaScript',
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png" /> image: `${SITE_URL}/share.png`,
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png" /> license: 'https://opensource.org/licenses/MIT',
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png" /> // It is genuinely free, and Google wants the price stated to believe it.
<meta name="apple-mobile-web-app-title" content="Favycon" /> offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> author: { '@type': 'Person', ...AUTHOR },
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> contributor: CONTRIBUTORS.map((person) => ({ '@type': 'Person', ...person })),
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> codeRepository: 'https://github.com/ruisaraiva19/favycon',
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" /> }
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png" />
<meta name="msapplication-TileColor" content="#ffffff" /> return (
<meta name="msapplication-TileImage" content="/favicon-144x144.png" /> <Head>
<meta name="msapplication-config" content="/browserconfig.xml" /> <title>{title}</title>
<meta property="og:title" content={title} /> <meta name="description" content={description} />
<meta property="og:description" content={description} /> <meta name="author" content={AUTHOR.name} />
<meta property="og:type" content="website" /> <link rel="author" href={AUTHOR.url} />
<meta property="og:site_name" content={title} /> {/* favycon.app has no DNS, so vercel.app is the only hostname this answers on. */}
<meta property="og:url" content="https://favycon.vercel.app" /> <link rel="canonical" href={SITE_URL} />
<meta property="og:image" content="https://favycon.vercel.app/share.png?v5" /> <link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png" />
<meta property="og:image:type" content="image/png" /> <link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png" />
<meta property="og:image:width" content="1200" /> <link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png" />
<meta property="og:image:height" content="630" /> <link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png" />
<meta name="twitter:card" content="summary_large_image" /> <link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png" />
</Head> <link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png" />
) <link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png" />
<meta name="apple-mobile-web-app-title" content="Favycon" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileImage" content="/favicon-144x144.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={title} />
<meta property="og:url" content={SITE_URL} />
<meta property="og:image" content={`${SITE_URL}/share.png?v5`} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
</Head>
)
}
export { SEO } export { SEO }

View File

@ -0,0 +1,22 @@
// Drawn from primitives rather than path data so the geometry is exact: a
// circle, the equator and one meridian. Stroked rather than filled, which is
// why the dark-mode rule for .personWebsite sets `stroke` where .personGithub
// sets `fill`.
const SvgWebsite: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width={18}
height={18}
viewBox="0 0 18 18"
fill="none"
stroke="currentColor"
strokeWidth={1.4}
strokeLinecap="round"
{...props}
>
<circle cx={9} cy={9} r={7} />
<line x1={2} y1={9} x2={16} y2={9} />
<ellipse cx={9} cy={9} rx={3.2} ry={7} />
</svg>
)
export { SvgWebsite }

View File

@ -1,7 +1,7 @@
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
.root { .root {
font-family: Manrope, Arial, Helvetica Neue, Helvetica, sans-serif; font-family: 'Manrope', Arial, Helvetica Neue, Helvetica, sans-serif;
} }
.h1 { .h1 {

15
cypress.config.ts Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from 'cypress'
export default defineConfig({
viewportWidth: 1280,
viewportHeight: 720,
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:3000',
},
})

View File

@ -1,5 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"viewportWidth": 1280,
"viewportHeight": 720
}

View File

@ -1,3 +1,4 @@
/** @type {import('next').NextConfig} */
module.exports = { module.exports = {
poweredByHeader: false, poweredByHeader: false,
images: { images: {
@ -5,28 +6,33 @@ module.exports = {
imageSizes: [56, 112, 185, 370, 370, 375, 540, 740, 750, 1080], imageSizes: [56, 112, 185, 370, 370, 375, 540, 740, 750, 1080],
}, },
headers() { headers() {
return [ return process.env.NODE_ENV === 'production'
{ ? [
source: '/(.*)',
headers: [
{ {
key: 'X-XSS-Protection', source: '/(.*)',
value: '1; mode=block', headers: [
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Content-Security-Policy',
value: `default-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://hive.splitbee.io https://vitals.vercel-insights.com; script-src 'self' https://cdn.splitbee.io; font-src 'self'; frame-ancestors 'self'`,
},
],
}, },
{ ]
key: 'X-Frame-Options', : []
value: 'SAMEORIGIN', },
}, eslint: {
{ ignoreDuringBuilds: true,
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Content-Security-Policy',
value: `default-src 'self' 'unsafe-inline'; connect-src 'self' https://hive.splitbee.io https://vitals.vercel-insights.com; script-src 'self' https://cdn.splitbee.io; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'self'`,
},
],
},
]
}, },
} }

View File

@ -18,7 +18,7 @@
], ],
"author": { "author": {
"name": "Rui Saraiva", "name": "Rui Saraiva",
"email": "ruisaraiva19@gmail.com", "email": "hello@ruisaraiva.dev",
"url": "https://ruisaraiva.dev/" "url": "https://ruisaraiva.dev/"
}, },
"license": "MIT", "license": "MIT",
@ -44,97 +44,101 @@
"dependencies": { "dependencies": {
"@fiahfy/ico-convert": "0.0.12", "@fiahfy/ico-convert": "0.0.12",
"@splitbee/web": "0.3.0", "@splitbee/web": "0.3.0",
"@use-gesture/react": "10.2.0", "@use-gesture/react": "10.2.16",
"adm-zip": "0.5.9", "adm-zip": "0.5.9",
"blurhash": "1.1.4", "blurhash": "1.1.5",
"classnames": "2.3.1", "classnames": "2.3.1",
"multer": "1.4.3", "multer": "1.4.3",
"next": "12.0.6", "next": "12.2.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"prismjs": "1.25.0", "prismjs": "1.28.0",
"prop-types": "15.7.2", "prop-types": "15.8.1",
"react": "17.0.2", "react": "17.0.2",
"react-clipboard.js": "2.0.16", "react-clipboard.js": "2.0.16",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-dropzone": "11.4.2", "react-dropzone": "11.4.2",
"react-modal": "3.14.4", "react-modal": "3.15.1",
"react-transition-group": "4.4.2", "react-transition-group": "4.4.2",
"react-use": "17.3.1" "react-use": "17.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.16.0", "@babel/core": "7.18.6",
"@commitlint/cli": "15.0.0", "@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "15.0.0", "@commitlint/config-conventional": "17.0.3",
"@storybook/addon-a11y": "6.4.7", "@storybook/addon-a11y": "6.5.9",
"@storybook/addon-actions": "6.4.7", "@storybook/addon-actions": "6.5.9",
"@storybook/addon-console": "1.2.3", "@storybook/addon-console": "1.2.3",
"@storybook/addon-docs": "6.4.7", "@storybook/addon-docs": "6.5.9",
"@storybook/addon-essentials": "6.4.7", "@storybook/addon-essentials": "6.5.9",
"@storybook/addon-links": "6.4.7", "@storybook/addon-links": "6.5.9",
"@storybook/addon-postcss": "2.0.0", "@storybook/addon-postcss": "2.0.0",
"@storybook/addons": "6.4.7", "@storybook/addons": "6.5.9",
"@storybook/api": "6.4.7", "@storybook/api": "6.5.9",
"@storybook/builder-webpack5": "6.4.7", "@storybook/builder-webpack5": "6.5.9",
"@storybook/client-api": "6.4.7", "@storybook/client-api": "6.5.9",
"@storybook/components": "6.4.7", "@storybook/components": "6.5.9",
"@storybook/core-events": "6.4.7", "@storybook/core-events": "6.5.9",
"@storybook/manager-webpack5": "6.4.7", "@storybook/manager-webpack5": "6.5.9",
"@storybook/node-logger": "6.4.7", "@storybook/node-logger": "6.5.9",
"@storybook/react": "6.4.7", "@storybook/react": "6.5.9",
"@storybook/theming": "6.4.7", "@storybook/theming": "6.5.9",
"@types/adm-zip": "0.4.34", "@types/adm-zip": "0.4.34",
"@types/multer": "1.4.7", "@types/multer": "1.4.7",
"@types/node": "16.11.11", "@types/node": "16.11.11",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/prismjs": "1.16.6", "@types/prismjs": "1.26.0",
"@types/react": "17.0.37", "@types/react": "17.0.37",
"@types/react-modal": "3.13.1", "@types/react-modal": "3.13.1",
"@types/react-transition-group": "4.4.4", "@types/react-transition-group": "4.4.5",
"@types/sharp": "0.29.4", "@types/sharp": "0.29.4",
"@typescript-eslint/eslint-plugin": "5.5.0", "@typescript-eslint/eslint-plugin": "5.30.5",
"@typescript-eslint/parser": "5.5.0", "@typescript-eslint/parser": "5.30.5",
"all-contributors-cli": "6.20.0", "all-contributors-cli": "6.20.0",
"babel-loader": "8.2.3", "babel-loader": "8.2.5",
"commitizen": "4.2.4", "commitizen": "4.2.4",
"cypress": "9.1.1", "cypress": "10.3.0",
"cypress-file-upload": "5.0.8", "cypress-file-upload": "5.0.8",
"cz-conventional-changelog": "3.3.0", "cz-conventional-changelog": "3.3.0",
"dotenv": "10.0.0", "dotenv": "16.0.1",
"eslint": "8.4.0", "eslint": "8.19.0",
"eslint-config-next": "12.0.6", "eslint-config-next": "12.2.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-cypress": "2.12.1", "eslint-plugin-cypress": "2.12.1",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-storybook": "0.5.3", "eslint-plugin-storybook": "0.5.13",
"husky": "7.0.4", "husky": "8.0.1",
"lint-staged": "12.1.2", "lint-staged": "13.0.3",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"postcss": "8.4.4", "postcss": "8.4.14",
"postcss-100vh-fix": "1.0.2", "postcss-100vh-fix": "1.0.2",
"postcss-flexbugs-fixes": "5.0.2", "postcss-flexbugs-fixes": "5.0.2",
"postcss-preset-env": "7.0.1", "postcss-preset-env": "7.7.2",
"postcss-scss": "4.0.2", "postcss-scss": "4.0.4",
"prettier": "2.3.2", "prettier": "2.7.1",
"react-refresh": "0.11.0", "react-refresh": "0.14.0",
"sass": "1.44.0", "require-from-string": "2.0.2",
"sass-loader": "12.3.0", "sass": "1.53.0",
"semantic-release": "18.0.1", "sass-loader": "13.0.2",
"semantic-release": "19.0.3",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"storybook-addon-next-router": "3.1.0", "storybook-addon-next-router": "4.0.0",
"storybook-dark-mode": "1.0.8", "storybook-dark-mode": "1.1.0",
"stylelint": "14.1.0", "stylelint": "14.9.1",
"stylelint-config-css-modules": "2.3.0", "stylelint-config-css-modules": "4.1.0",
"stylelint-config-prettier": "9.0.3", "stylelint-config-prettier": "9.0.3",
"stylelint-config-recess-order": "3.0.0", "stylelint-config-recess-order": "3.0.0",
"stylelint-config-standard": "24.0.0", "stylelint-config-standard": "26.0.0",
"stylelint-prettier": "2.0.0", "stylelint-prettier": "2.0.0",
"stylelint-scss": "4.0.0", "stylelint-scss": "4.2.0",
"typescript": "4.4.4", "typescript": "4.7.4",
"webpack": "5.64.4" "webpack": "5.73.0"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "cz-conventional-changelog" "path": "cz-conventional-changelog"
} }
},
"resolutions": {
"sharp": "^0.28.0"
} }
} }

View File

@ -4,16 +4,13 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document { class MyDocument extends Document {
render() { render() {
return ( return (
<Html lang="en"> <Html lang="en" className="preload">
<Head> <Head />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <body>
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" /> <script src="/js/noflash.js?v1" />
</Head>
<body className="preload">
<script src="/js/noflash.js" />
<Main /> <Main />
<NextScript /> <NextScript />
<script src="/js/onload.js" /> <script src="/js/onload.js?v1" />
</body> </body>
</Html> </Html>
) )

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -1,5 +1,5 @@
;(function () { ;(function () {
window.addEventListener('load', function () { window.addEventListener('load', function () {
document.body.classList.remove('preload') document.documentElement.classList.remove('preload')
}) })
})() })()

48
public/llms.txt Normal file
View File

@ -0,0 +1,48 @@
# Favycon
> A free online tool that turns one image into a complete set of favicons, in
> every size and format a site needs, as a downloadable zip.
Drag and drop an image and Favycon returns `favicons.zip` plus the markup to
paste into your `<head>`. Nothing is stored: the image is converted in the
request and the zip is streamed straight back.
Site: https://favycon.vercel.app
## Input
- Accepted formats: JPEG, PNG, SVG.
- Minimum dimensions: 310x310. SVG input is exempt, since it is rescaled from
vector.
- With the PWA option enabled the minimum is 512x512, again except for SVG.
## What the zip contains
- `icons/favicon-<size>.png` at 16x16, 32x32, 57x57, 60x60, 70x70, 72x72,
76x76, 96x96, 114x114, 120x120, 144x144, 150x150, 152x152, 180x180, 192x192
and 310x310.
- `icons/favicon.ico`, rendered from a 256x256 master.
- `icons/favicon.svg`, only when the uploaded image was itself an SVG.
- `icons/browserconfig.xml` for IE10+ tiles.
- `icons/manifest.json`, only when the PWA option is on. That option also adds
128x128, 384x384 and 512x512 PNGs.
- `readme.txt` with the HTML to add, written to match the options chosen.
## Facts
- Author: Rui Saraiva (https://ruisaraiva.dev)
- Contributors: Augusto Lopes (product design), Miguel Teixeira
- Built in 2020
- Licence: MIT
- Source: https://github.com/ruisaraiva19/favycon
- Price: free, no account, no upload limit published
- Contact: hello@ruisaraiva.dev
## Notes
- This is a single-page tool. There is no blog, docs site or API documentation
to crawl beyond this file and the repository.
- There is a public HTTP endpoint at `POST /api/favycon`. It takes a multipart
body with an `icon` file field and an optional `pwa` field set to `1`, and
responds with `application/zip`. It is what the page itself calls; it is not
a documented or versioned public API.

View File

@ -1,2 +1,8 @@
User-Agent: * User-Agent: *
Allow: / Allow: /
# Nothing is disallowed. /api/favycon is POST-only and answers a GET with 405,
# which is a clearer signal to a crawler than a rule it has to fetch robots.txt
# to learn. A plain-text description of the tool lives at /llms.txt
Sitemap: https://favycon.vercel.app/sitemap.xml

6
public/sitemap.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://favycon.vercel.app</loc>
</url>
</urlset>

View File

@ -1,48 +1,48 @@
@font-face { @font-face {
font-family: Manrope; font-family: 'Manrope';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('https://fonts.gstatic.com/s/manrope/v4/xn7gYHE41ni1AdIRggexSvfedN4.woff2') format('woff2'); src: url('/fonts/manrope-v12.woff2') format('woff2');
font-display: swap; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@font-face { @font-face {
font-family: Manrope; font-family: 'Manrope';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url('https://fonts.gstatic.com/s/manrope/v4/xn7gYHE41ni1AdIRggexSvfedN4.woff2') format('woff2'); src: url('/fonts/manrope-v12.woff2') format('woff2');
font-display: swap; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@font-face { @font-face {
font-family: Manrope; font-family: 'Manrope';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('https://fonts.gstatic.com/s/manrope/v4/xn7gYHE41ni1AdIRggexSvfedN4.woff2') format('woff2'); src: url('/fonts/manrope-v12.woff2') format('woff2');
font-display: swap; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@font-face { @font-face {
font-family: Manrope; font-family: 'Manrope';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url('https://fonts.gstatic.com/s/manrope/v4/xn7gYHE41ni1AdIRggexSvfedN4.woff2') format('woff2'); src: url('/fonts/manrope-v12.woff2') format('woff2');
font-display: swap; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@font-face { @font-face {
font-family: Manrope; font-family: 'Manrope';
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: url('https://fonts.gstatic.com/s/manrope/v4/xn7gYHE41ni1AdIRggexSvfedN4.woff2') format('woff2'); src: url('/fonts/manrope-v12.woff2') format('woff2');
font-display: swap; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;

View File

@ -11,14 +11,14 @@ body {
/* Disables pull-to-refresh but allows overscroll glow effects. */ /* Disables pull-to-refresh but allows overscroll glow effects. */
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
font-family: Manrope, Arial, 'Helvetica Neue', Helvetica, sans-serif; font-family: 'Manrope', Arial, 'Helvetica Neue', Helvetica, sans-serif;
/* https://gist.github.com/hsleonis/55712b0eafc9b25f1944 */ /* https://gist.github.com/hsleonis/55712b0eafc9b25f1944 */
font-size: 100%; font-size: 100%;
font-variant-ligatures: none; font-variant-ligatures: none;
text-shadow: rgba($black, 0.01) 0 0 1px; text-shadow: rgba($black, 0.01) 0 0 1px;
transition: background 0.2s ease; transition: background 0.2s ease;
text-rendering: optimizeLegibility; text-rendering: optimizelegibility;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

6457
yarn.lock

File diff suppressed because it is too large Load Diff